React Native 条目式字典:组件 → 样式 → Hooks → 列表 → 导航 → 构建。每条给语义、示例、边界与坑。
1. 核心组件对照(RN ↔ Web)
描述
RN 没有 DOM,组件是原生视图的封装。关键差异:文字必须在 Text 内,Image 网络图必须定尺寸,长列表必须 FlatList。
语法和示例
<table>
<thead><tr><th>RN</th><th>Web 对应</th><th>关键约束</th></tr></thead>
<tbody><tr><td>View</td><td>div</td><td>布局容器;不支持语义样式继承</td></tr><tr><td>Text</td><td>p / span</td><td>一切文字必须在它内;嵌套 Text 才继承样式</td></tr><tr><td>Image</td><td>img</td><td>网络图必须 width/height;本地用 require</td></tr><tr><td>Pressable</td><td>button</td><td>统一按压反馈(取代 Touchable 系)</td></tr><tr><td>FlatList</td><td>—</td><td>长列表虚拟化(必须)</td></tr><tr><td>ScrollView</td><td>overflow:scroll</td><td>全量渲染,短内容才用</td></tr><tr><td>TextInput</td><td>input</td><td>受控 value + onChangeText</td></tr><tr><td>SafeAreaView</td><td>—</td><td>刘海/底部安全区(safe-area-context)</td></tr></tbody></table>注意事项
- Text 外放字符串会直接崩(RN 校验严格)
- Image 不定尺寸等于 0x0(网络图不会自动撑开)
- ScrollView 内放长列表会让性能崩塌——必须 FlatList
- Pressable 比 TouchableOpacity 更可控(支持按压状态样式)
- RN 没有 CSS 级联:样式不继承(Text 例外)
2. Flexbox 默认差异与样式规则
描述
RN 的 Flexbox 默认 flexDirection: ‘column’(Web 是 row);尺寸无单位(iOS 是 pt,Android 是 dp);没有 px/% 混用的概念。
语法和示例
const s = StyleSheet.create({
row: {
flexDirection: 'row', // 必须显式
alignItems: 'center',
justifyContent: 'space-between',
gap: 8, // 0.71+ 支持 gap
},
card: { padding: 16, borderRadius: 12, backgroundColor: '#fff' },
});
// 百分比:'100%' 只支持部分场景
width: '100%';
flex: 1; // 占满剩余空间
// 阴影平台差异
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8 },
android: { elevation: 4 },
});注意事项
- 默认 column 是最常见踩坑点——从 Web 转过来的人总要显式写 row
- 没有 CSS 继承:fontSize/color 要在每个 Text 上设(或用包装组件)
- margin/padding 负数可用于视觉偏移,但会影响触摸区域
- 阴影在两端实现完全不同(iOS shadow*,Android elevation)
- StyleSheet.create 有性能收益(创建一次复用),但样式仍是普通对象
3. useState 函数式更新
描述
依赖旧值时必须传函数形式,否则批量更新下会丢更新。这与 React 的批处理机制有关。
语法和示例
// 正确:函数式更新
setCount(c => c + 1);
setItems(list => [...list, item]);
// 错误:连续两次点只加一次
setCount(count + 1);
setCount(count + 1);
// 对象合并
setForm(f => ({ ...f, name: v }));
// 条件更新
setItems(list => list.filter(x => x.id !== id));注意事项
- 循环里连续 setState 用非函数式会只生效最后一次
- RN 的批处理与 React 18 一致(自动批处理),但异步回调里仍可能分批
- setState 是异步的,紧接着读 state 拿到旧值——要用 useEffect 或局部变量
- 大对象/大数组更新前先判等,避免无谓渲染
- 不要直接把 state 对象改内部字段(引用不变导致不重渲染)
4. useEffect 竞态与清理
描述
副作用(请求、订阅、定时器)。清理函数在卸载/依赖变化时执行,用于取消请求与解绑。
语法和示例
useEffect(() => {
let dead = false;
fetch(url).then(r => r.json())
.then(d => { if (!dead) setData(d); });
return () => { dead = true; }; // 卸载后不写状态
}, [url]);
// AbortController 版
useEffect(() => {
const ac = new AbortController();
fetch(url, { signal: ac.signal })
.then(r => r.json()).then(setData)
.catch(e => { if (e.name !== 'AbortError') log(e); });
return () => ac.abort();
}, [url]);
// 订阅清理
useEffect(() => {
const sub = NetInfo.addEventListener(handle);
return () => sub();
}, []);注意事项
- 请求后未防竞态:快速切换页面会导致旧请求覆盖新数据
- 清理函数必须取消订阅/定时器,否则内存泄漏与重复执行
- 依赖数组漏项会导致闭包拿到旧值(ESLint 的 exhaustive-deps 规则能抓)
- Effect 里不要用 async 函数(返回 Promise 而非清理函数)——内部包一层
- 空依赖 [] 只跑一次;不写依赖每次都跑(性能杀手)
5. FlatList 性能 props
描述
长列表虚拟化的关键参数:keyExtractor、getItemLayout、windowSize、initialNumToRender、onEndReached。
语法和示例
<FlatList
data={rows}
keyExtractor={(r) => r.id}
getItemLayout={(_, i) => ({ length: ROW_H, offset: ROW_H * i, index: i })}
initialNumToRender={12}
windowSize={9} // 默认 21,降低换内存
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
removeClippedSubviews // Android 收益大,长列表可能白屏
onEndReached={loadMore}
onEndReachedThreshold={0.4}
refreshControl={<RefreshControl refreshing={loading} onRefresh={reload} />}
/>注意事项
- 缺 keyExtractor 会用 index(增删导致错位与重渲染)
- getItemLayout 只适用定高行——不定高时别传(会滚动错位)
- windowSize 影响渲染范围,太小会白屏(滚动快时来不及渲染)
- removeClippedSubviews 在某些 Android 版本有白屏 bug,需实测
- renderItem 内不要创建新函数/对象(每行都重建)——提到外层或用 useCallback
6. 导航:expo-router 与 React Navigation
描述
expo-router 是文件式路由(推荐给新项目);React Navigation 是底层库(Stack/Tabs/Drawer)。参数传递与返回结果都要显式处理。
语法和示例
// expo-router:文件即路由(app/ 目录)
app/index.tsx → /
app/about.tsx → /about
app/[id].tsx → /:id
// 跳转与参数
import { useRouter, useLocalSearchParams } from 'expo-router';
const router = useRouter();
router.push(`/detail/${id}`);
const { id } = useLocalSearchParams<{ id: string }>();
// React Navigation
navigation.navigate('Detail', { id: 42 });
const { id } = route.params;
// Android 返回键
BackHandler.addEventListener('hardwareBackPress', () => { return true; });注意事项
- URL 参数是字符串,id 要自己转数字
- expo-router 的路由文件必须在 app/ 目录且命名符合约定(_layout/tabs/[id])
- 返回上一屏带结果需用 router 的 params 或状态管理,不是直接 return
- Android 的物理返回键默认退出应用,需要拦截时返回 true
- 深链与通知跳转都要在根 layout 里统一处理
7. 平台分叉与安全区
描述
平台差异:Platform.select 与文件后缀(.ios.tsx/.android.tsx);安全区用 safe-area-context。
语法和示例
import { Platform, useWindowDimensions } from 'react-native';
const pad = Platform.select({ ios: 10, android: 8, default: 8 });
// 文件级分叉:Button.ios.tsx / Button.android.tsx 自动按平台加载
// 安全区
import { SafeAreaView } from 'react-native-safe-area-context';
<SafeAreaView edges={['top', 'bottom']}>...</SafeAreaView>
// 响应旋转
const { width, height } = useWindowDimensions();注意事项
- Platform.OS 是运行时判断,文件后缀是编译时分离——两者都可用但用途不同
- iOS 与 Android 的阴影/字体/触摸反馈差异较大,关键 UI 要分别调
- 安全区必须用 safe-area-context(RN 内置的 SafeAreaView 已不推荐)
- useWindowDimensions 必须用 Hook(Dimensions.get 不响应旋转)
- 鸿蒙/Weex 等非标准平台需额外适配,别默认 RN 全兼容
8. 构建与发布
描述
Expo EAS 云构建(Windows 也能出 iOS 包);本地 Android 用 Gradle;OTA 更新只覆盖 JS 层。
语法和示例
bash
1. EAS(推荐,跨平台)
eas build --platform android
eas build --platform ios # 云端 macOS 构建
1. 本地 Android
cd android && ./gradlew assembleRelease
1. 版本号
1. android/app/build.gradle: versionCode / versionName
1. ios: CFBundleShortVersionString / CFBundleVersion注意事项
- Windows 无法本地构建 iOS 包(必须 macOS 或 EAS 云构建)
- OTA 更新(Expo Updates)只更新 JS 与资源,原生代码变更必须发版
- versionCode 每次上传都要递增,否则商店拒绝
- 生产签名密钥必须妥善保存(丢失后无法更新已发布应用)
- aab 上 Google Play,apk 用于内测分发;iOS 需 TestFlight 或 Ad Hoc