常见陷阱与最佳实践
这一章是整个手册的收口:把散在 00~11 章的坑汇总成一张速查表,再给出一份 2026 年视角的"旧教程纠偏清单"、最佳实践和上线自检。写 RN 应用出事前,先来这里对一遍。
基线:React Native 0.88 / Expo SDK 58 / TypeScript。版本号均为 2026-10 实测(npm registry、官方仓库、Expo changelog),不确定的写法在正文已标注"以当期文档为准"。
一、陷阱汇总表
环境与版本
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 还在用 Node 20 | CLI 报错、构建诡异失败 | RN 0.87/0.88 只认 ^22.13 || ^24.3 || >=26;Expo 58 同口径且不支持 23/25 奇数线 |
00-安装与快速开始.md |
| 被模板 engines 误导 | 装了 22.11/22.12 | bare 模板 package.json 写 >=22.11,但 react-native core engines 是 ^22.13.0——按高的那个取 |
00 |
| react 与 react-native 没配对 | 启动即崩、Invalid hook call |
0.88 必须精确配 React 19.3.0;Expo 项目一律 npx expo install,别用 npm i 装 RN 生态包 |
00、01 |
| 按 2023 年教程开旧架构开关 | newArchEnabled=false 无效甚至报错 |
0.82 起旧架构已整体移除,没有回头路;报错方向是查库的新架构兼容性 | 01、09 |
| 用旧教程的初始化命令 | react-native init 已死 |
bare 用 npx @react-native-community/cli@latest init;官方推荐路径其实是 npx create-expo-app |
00 |
组件与列表
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 裸字符串放进 View | 红屏/运行时报错 | 一切文字必须包在 <Text> 里 |
02 |
FlatList extraData 传可变数组 |
选中态不刷新,且无任何报错 | extraData 走引用比较;push 后引用不变等于没更新,改用不可变更新(setState 新数组) |
02 |
ListHeaderComponent={<SearchBar />} 行内 JSX |
每次父渲染整块 header 重建、列表跳动 | header 元素 useMemo 固定引用,或直接传组件 |
02 |
| ScrollView 当长列表用 | 内存随数据量线性膨胀、滚动掉帧 | 超过十几行就换 FlatList;虚拟化是底线 | 02 |
Modal 忘配 onRequestClose |
iOS 一切正常,Android 用户被返回键困死 | 必传;这是平台不对称坑的头号代表 | 02 |
用 RN 自带 SafeAreaView |
仅 iOS 生效、Android 贴刘海 | 官方已标注弃用,且 bare 0.88 模板直接预装 react-native-safe-area-context——用它 |
02、03 |
| 用 Touchable 系列写新代码 | 老 API、行为差异多 | 统一 Pressable;onPress 是抬起、onPressIn 是按下,别混 |
02、06 |
Android 上 removeClippedSubviews 期望过高 |
快速滚动白屏或无效 | 它只对"内容远大于视口且高度固定"场景有收益,先做好 getItemLayout | 02 |
样式与布局
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
按 Web 心智设 flexDirection |
布局全变竖的 | RN 默认 column,Web 默认 row |
03 |
| 等 CSS 继承 | 子 View 文字样式全丢 | 文本样式只沿 <Text> 嵌套链传递 |
03 |
Dimensions.get 算一次存全局 |
旋转/分屏后尺寸过期 | 用 useWindowDimensions,跟着屏幕走 |
03 |
| iOS 阴影配方原样搬 Android | Android 上阴影消失或一团黑 | Android 用 elevation 近似;阴影双写是常态 |
03 |
| edge-to-edge 时代还指望状态栏特例 | Android 15+ 强制内容延伸到系统栏后 | 0.86 是 RN 的适配主版本、0.88 模板默认 edgeToEdgeEnabled=true;安全区交给 react-native-safe-area-context |
03、04 |
用 onLayout 同步拿尺寸 |
首帧拿到 0 或延迟值 | onLayout 是异步事件且只在挂载时触发一次;测量用 ref.measureInWindow 并做好回调 |
03 |
导航与路由
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 以为屏内切屏会重新挂载 | navigate 回已有屏,useEffect 不跑 |
同屏实例复用;“切回就刷新"用 useFocusEffect,“参数变就刷新"把 params 进依赖数组 |
04 |
| 列表→详情传整个对象 | 参数膨胀、栈内存吃紧、深链对不上 | 传 id,详情页自己取数(和 05 的 Query 缓存配合) |
04 |
认证流靠 navigate 后清栈 |
登出后返回键还能回用户页 | 条件渲染 screen 树 + reset,从根上不让受保护屏入栈 |
04 |
| Android 硬件返回键裸 Modal/自绘弹层 | 返回键直接退 App 或无响应 | BackHandler 注册并记得卸载时移除监听 |
04 |
网络与数据
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
onEndReachedThreshold 不设防 |
一次滑动连发多页重复请求 | 默认 0.5(距底半屏)就会触发;分页必须 hasNextPage && !isFetchingNextPage 双守卫 |
05 |
| 下拉刷新与分页守卫混用 | 下拉时分页卡死(或反之) | isRefetching 归下拉、isFetchingNextPage 归分页,别互相顶替 |
05 |
| fetch 不配超时 | 弱网下请求无限悬挂 | RN 的 fetch 没有原生超时参数;AbortController + setTimeout 自己包 |
05 |
| 组件卸载后 setState | 警告(旧)/静默内存驻留(新) | effect 里返回 cleanup,AbortController.abort() 一并取消 |
05 |
FormData 按浏览器习惯传 File |
上传拿不到文件 | RN 的约定是 { uri, name, type } 对象形态 |
05 |
Android 模拟器里写 localhost |
127.0.0.1 连的是模拟器自己 | 10.0.2.2;真机开发用局域网 IP 或 Expo tunnel |
05 |
存储
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 几百 KB 的 JSON 塞 AsyncStorage | JS 线程被解析卡住 | 大对象拆分或上 SQLite;KV 只放配置级数据 | 08 |
| MMKV 同 key"push 一条” | 整表被静默覆盖 | MMKV 的 set 是纯覆盖写,没有追加语义;读-改-写走事务化函数 | 08 |
| 可再生缓存写进 documents 目录 | 用户 iCloud 被备份爆仓、审核风险 | cache 目录不参与备份;documents 会被云备份 | 08 |
| token 存 AsyncStorage | 明文落盘 | 安全存储走 Keychain/Keystore:expo-secure-store(注意单值大小量级限制,别塞大 JSON) |
08 |
| 按 v3 教程初始化 MMKV | 找不到 createMMKVStorage |
v4 工厂统一为 createMMKV(),且需要 react-native-nitro-modules peer 依赖(registry 实测) |
08 |
动画与手势
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| worklet 里闭包捕获普通 JS 值/函数 | UI 线程运行时直接抛错或行为诡异 | 跨线显式 runOnJS;shared value 是唯一常规通道 |
07 |
| 动画里 setState | 每帧一次 JS 渲染,60fps 变 20fps | 状态变更走 shared value,React 渲染不参与动画 | 07 |
| width/height 动画想走原生驱动 | 掉帧或报错 | 只有 transform 三件套(translate/scale/rotate)与 opacity 类能上原生驱动;尺寸动画改用 Reanimated layout animation 或重排方案 | 07 |
| 手势嵌套 ScrollView 冲突 | 拖拽抢不过滚动 | Gesture Handler 的激活条件与 simultaneousHandlers 显式声明;GestureHandlerRootView 要包在根上 |
07 |
| jest 里 import reanimated | 测试直接炸 | 需要按库文档注入 mock(形态随版本变过,以当期测试章节为准) | 07、10 |
表单与输入
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 受控 TextInput 撞中文输入法 | Android 拼音候选期间回灌、候选闪烁/删字 | RN 无 composition 事件,平台级限制;长中文文本改非受控或提交时再校验 | 06 |
同一输入框动态切 keyboardType |
键盘闪烁、焦点丢失 | 需要切类型就拆成两个输入框(如手机号/验证码) | 06 |
| KeyboardAvoidingView 平台一刀切 | iOS 键盘弹出错位、Android 双重补偿 | iOS behavior="padding";Android 优先 windowSoftInputMode="resize" 并跳过 KAV |
06 |
以为 multiline 有 autoGrow |
prop 不存在、不生效 | 官方源码无此属性;onContentSizeChange 夹高度自实现 |
06 |
密码框忘配 textContentType |
密码管理器/自动填充失效、审核扣分 | 值是 password / newPassword(不是 passwordNew,旧文章写错的那批);配合 autoCapitalize="none" |
06 |
用 InputAccessoryView 做键盘工具条 |
0.88 已弃用,红字警告 | 迁移到 Keyboard Avoiding + 自绘悬浮条或社区方案 | 06 |
Android 13+ 只申请 READ_EXTERNAL_STORAGE |
拿不到相册、权限弹窗缺失 | 细分到 READ_MEDIA_IMAGES/VIDEO;iOS 的 Info.plist 用途文案是审核拒项 |
06 |
原生模块与架构
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| TurboModule 名字三处不一致 | 运行期拿到 null/抛异常,没有编译错误 | JS getEnforcing 参数、Android getName()、iOS 导出宏里的名字必须逐字相同 |
09 |
| 改完原生代码等 Fast Refresh | 刷新八百遍行为不变 | 原生改动必须全量重建 run:ios/run:android |
09 |
| 链接问题先猜注册错 | 浪费半天 | 先 npx react-native config 看发现链路,再 TurboModuleRegistry.get 探测可见性,最后进断点 |
09 |
| 存量库不体检就升 RN 大版 | 某个桥时代库在 0.82+ 集体爆 | 升级前逐个核对新架构兼容状态;0.88 的桥兼容层开关是"未来的刀”,别拿它当长期饭票 | 09 |
调试与测试
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| 教程让你开 Chrome remote debugging / 装 Flipper | 找不到入口/文档对不上 | 0.76 起统一用 React Native DevTools,Metro 终端按 d |
10 |
jest 没配 transformIgnorePatterns |
一跑测试满屏 SyntaxError(flow/TS 语法) | RN 生态包大多发未编译源码,白名单放行 react-native 相关包是标准配方 | 10 |
release 包崩溃栈全是 anonymous |
线上事故无法定位 | Hermes 字节码需要 source map/dSYM 符号化;构建产物归档 mapping,上报服务(Sentry/Expo Observe)收口 | 10、11 |
@react-native/jest-preset 与 RN 版本错配 |
preset 行为对不上运行时 | 0.85 起独立发版,跟着 RN 主版本一起升 | 10 |
构建与发布
| 陷阱 | 症状 | 解法 | 详见 |
|---|---|---|---|
| keystore 只存一台机器/进了 git | 双输:要么丢=更新断供,要么泄露 | 双备份、双人可及、绝不进仓库;Play App Signing 也救不了签名密钥丢失 | 11 |
| OTA 热更当万能 | 原生改动推出去当场炸 | EAS Update 只送 JS/资源;runtimeVersion policy 选定就别中途换(appVersion 会把新 JS 推给不同原生的老壳) |
11 |
Android versionCode 忘了加 |
商店拒收/覆盖上传失败 | 每次必增,进发版 checklist | 11 |
| R8/混淆问题只在 release 复现 | debug 一切正常,线上类找不到 | release 构建必须真机全链路冒烟,mapping.txt 归档 | 11 |
| 提审卡在政策与权限文案 | 反复被拒浪费一周 | 相机/相册/定位用途文案提前写好;审核窗口避开周五之后 | 11 |
二、旧教程纠偏清单(2026 版)
网上教程与模型记忆大量停留在 2022~2024,下表的"旧写法"今天照抄会直接错。这一节同样适合排查"AI 生成的 RN 代码"时对照。
| 旧教程写法 | 现状 | 依据 |
|---|---|---|
react-native init / react-native-community/cli 全局安装 |
bare 用 npx @react-native-community/cli@latest init;官方推荐其实是 Expo |
00 |
Android 阴影 shadowOpacity/shadowRadius 双端通用 |
Android 走 elevation(或新版本 boxShadow 路线),iOS 走 shadow* |
03 |
TouchableHighlight/TouchableOpacity |
统一 Pressable |
02 |
RN 自带 SafeAreaView |
已标注弃用;用 react-native-safe-area-context(0.88 模板预装) |
02 |
从 RN 导入 Picker / CheckBox |
早已移出核心到社区包 | 06 |
InputAccessoryView 做键盘辅助条 |
RN 0.88 弃用(官方 changelog 实测) | 06 |
import 'react-native/Libraries/Core/InitializeCore' |
RN 0.88 弃用,改 react-native/setup-env(官方 changelog 实测) |
01 |
import Text from 'react-native/Libraries/Components/Text/Text' 深导入 |
0.87 起 Strict TypeScript API 默认,深导入是类型错误 | 01、11 |
newArchEnabled: false 回退旧架构 |
0.82 起旧架构不存在,flag 无意义 | 01 |
| Flipper 做调试中枢 | 0.76 起模板移除,React Native DevTools(按 d)接班 |
10 |
| “iOS 需要自己编译 Hermes 源码” | 0.84 起 iOS 默认预编译 RN 框架二进制,pod install 分钟级 | 00 |
createMMKVStorage() 初始化 MMKV |
v4 统一 createMMKV(),并新增 react-native-nitro-modules peer(registry 实测) |
08 |
textContentType: 'passwordNew' |
正确值是 newPassword(源码实测) |
06 |
multiline + 期望存在 autoGrow |
TextInput 无此属性(源码实测);用 onContentSizeChange 夹高 |
06 |
| Node 18/20 够用 | RN 0.87+ 的 core engines 已踢出 Node 20 | 00 |
三、最佳实践清单
项目与依赖
- 新项目默认走 Expo(官方推荐),对原生工程强控制的场景再选 bare——两条路的退路都是通的:Expo 可 prebuild 出完整原生工程,bare 也可随时接入 Expo 生态的单包。
- Node 版本锁进仓库(volta/.nvmrc),CI、开发机、同事三处一致;react/react-native 版本配对表贴在 README。
npx expo install(Expo)或对照官方模板(bare)管理依赖版本,避免"npm i 装出 peer 漂移"。- 第三方库当成本金:升级前核对新架构兼容与近期维护活跃度;无新架构适配的库当作替换对象,不是等待对象。
- 深导入一律不用,0.87 起它就是类型错误;组件、hooks 全从
react-native顶层取。
性能
- 首屏预算倒推:bundle 体积(延迟加载非首屏代码)、启动链路(Hermes 字节码是默认优势,别再关)、列表虚拟化。
- 列表三板斧:稳定的
keyExtractor、renderItem提到组件外或用useCallback、行内容不可变更新;高频滚动列表再考虑社区虚拟化方案(如 FlashList)。 - 动画铁律:能进原生驱动就进原生驱动;手势驱动动画交给 Reanimated 的 UI 线程运行时,绝不在动画里 setState。
- 图片是内存大户:尺寸按显示需求下发(服务端出图或 CDN 参数),列表图片配
resizeMode与稳定 key。 - 别信"感觉不卡":DevTools Profiler + 系统级工具(Android Studio Profiler / Xcode Instruments)看证据。
稳定性与安全
- 每个 effect 的 cleanup 当作军规:监听器(Keyboard/Dimensions/AppState)、abort、定时器——泄漏三兄弟。
- 请求层集中做:超时、token 注入、错误归一化;业务代码不裸写 fetch。
- token 只进系统级安全存储(Keychain/Keystore),不进出 AsyncStorage/MMKV 明文档。
- 崩溃上报收口在构建里:source map/mapping.txt/dSYM 与构建产物一一对应归档;Expo 项目 Observe 已覆盖原生崩溃上报(AppMetrics 的后继)。
- 权限文案当产品文案写:Android 13+ 媒体细分权限、iOS Info.plist 用途描述,都是审核拒项高发区。
协作与发布
versionCode只增不减、发布分支打 tag、runtimeVersion policy 一个项目定一次。- keystore 双备份 + 双人可及;签名事故没有第二次机会。
- 发版排期把"等审核"和"48 小时观察窗"占正式格子;OTA 全量切换只在有人值守时段。
- 升级节奏:追 Expo SDK 大版本比追 RN 双月版现实(对应关系见下表);RN 是 0.88 这种"无破坏性小步快跑"节奏,跨 2~3 个版本一次性升,配合官方 upgrade helper 思路逐文件核 diff。
四、版本配对速查(实测)
RN 与 React 的配对(npm peerDependencies 实测):
| RN | React | Node(core engines) | Expo SDK |
|---|---|---|---|
| 0.78 / 0.79 | ^19.0.0 | >=18 | — |
| 0.80 | ^19.1.0 | >=18 | — |
| 0.81 | ^19.1.0 | >=20.19.4 | 54 |
| 0.82 | ^19.1.1 | >=20.19.4 | — |
| 0.83 | ^19.2.0 | >=20.19.4 | 55 |
| 0.84 | ^19.2.3 | >=20.19.4 | — |
| 0.85 | ^19.2.3 | ^20.19.4 || ^22.13 || ^24.3 || >=25 | 56 |
| 0.86 | ^19.2.3 | 同上 | 57 |
| 0.87 | ^19.2.3 | ^22.13 || ^24.3 || >=26 | — |
| 0.88 | ^19.3.0 | ^22.13 || ^24.3 || >=26 | 58 |
(React/Node 两列均为 npm registry 的 peerDependencies.react 与 engines.node 逐项实测,2026-10-20。)
要点:RN 约 2 个月一版;Expo SDK 挑版本上车(54→0.81、55→0.83、56→0.85、57→0.86、58→0.88),中间 RN 版本由 Expo 吸收。生产项目按 Expo SDK 线走,bare 项目按"季度追一次 RN minor"走。
五、上线自检清单
发版前逐项打钩:
- Node/RN/React 版本三者配对,
npx expo install --check(Expo)或 lockfile 审查(bare)通过 - 所有依赖库确认新架构兼容(0.82+ 没有旧架构退路)
- 深导入清零(0.87 Strict TS API 下编译干净)
- Android targetSdk/compileSdk 对齐当期商店政策与模板值(0.88 模板实测 targetSdk 36、compileSdk 37)
- edge-to-edge 双端真机过一遍:状态栏/导航栏内容不被遮、系统栏图标可读
- 深色模式全页巡览(useColorScheme 路径 + 图片双资源)
- 平板/折叠屏/大字体无障碍各截图一次(accessibility 是审核项)
- 列表页:空态/加载/错误/分页守卫四态齐;长列表 60fps 有 profiler 证据
- 表单页:中文输入法回归测试、键盘遮挡双平台截图、防重复提交生效
- 网络:飞行模式/弱网下无无限悬挂(超时封装生效)、下拉刷新与分页互不锁死
- 敏感数据不在明文 KV;权限文案双端齐全
- release 构建真机冒烟(R8/Hermes 字节码只在 release 暴露问题)
- 崩溃符号化链路演练:拿一条 release 崩溃能还原到行
- keystore/证书/EAS 凭证就位,runtimeVersion policy 与上次发布一致
- OTA 边界确认:本次版本有无原生改动,update channel 目标正确
- 48 小时观察窗排期落格,回滚触发阈值写进发布记录
六、自测题
- 为什么 FlatList 的
extraData={arr}配arr.push(x)不刷新?正确的最小修法是什么?(02) - RN 0.82 之后还能不能"退回旧架构"?如果某个库只有桥时代实现,你的三条出路是什么?(
01、09) - 分页列表为什么必须
hasNextPage && !isFetchingNextPage双守卫?只用其中一个分别会怎样?(05) - EAS Update 推了一次含原生依赖升级的包,会发生什么?怎么用流程杜绝?(
11) - 用户反馈"拼音打字时输入框闪烁删字",这是 bug 吗?你有哪些现实缓解手段?(
06) - 同一个 TurboModule 在 Android 能拿到、iOS 是 null,排查顺序是什么?(
09) - 写一条旧→新对照:
react-native init、Flipper、newArchEnabled=false、createMMKVStorage分别对应今天什么?(本章二节)
实战建议
把这本手册当"版本敏感"文档读:凡涉及具体版本号,以本文件第四节的实测表为准;凡涉及三方库 API(Reanimated、Gesture Handler、MMKV、Expo 各包),它们的版本线各自独立,动手前查当期官方文档的对应章节。RN 这两年的变化密度很高(旧架构移除、Hermes V1、Strict TS、edge-to-edge),2023 年以前的教程与代码生成结果只能当思路参考,不能当事实来源——这正是本手册把"纠偏清单"放在收口的原因。