react-native

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Native Patterns

React Native 开发模式

Performance and architecture patterns for React Native + Expo apps. Rules ranked by impact — fix CRITICAL before touching MEDIUM.
This is a starting point. The skill will grow as you build more mobile apps.
适用于React Native + Expo应用的性能与架构模式。规则按影响程度排序——优先修复CRITICAL(严重)问题,再处理MEDIUM(中等)问题。
这是一个起点,随着你开发更多移动应用,这些技巧也会不断扩充。

When to Apply

适用场景

  • Building new React Native or Expo apps
  • Optimising list and scroll performance
  • Implementing animations
  • Reviewing mobile code for performance issues
  • Setting up a new Expo project
  • 构建新的React Native或Expo应用
  • 优化列表与滚动性能
  • 实现动画效果
  • 评审移动应用代码以排查性能问题
  • 搭建新的Expo项目

1. List Performance (CRITICAL)

1. 列表性能(CRITICAL)

Lists are the #1 performance issue in React Native. A janky scroll kills the entire app experience.
PatternProblemFix
ScrollView for data
<ScrollView>
renders all items at once
Use
<FlatList>
or
<FlashList>
— virtualised, only renders visible items
Missing keyExtractorFlatList without
keyExtractor
→ unnecessary re-renders
keyExtractor={(item) => item.id}
— stable unique key per item
Complex renderItemExpensive component in renderItem re-renders on every scrollWrap in
React.memo
, extract to separate component
Inline functions in renderItem
renderItem={({ item }) => <Row onPress={() => nav(item.id)} />}
Extract handler:
const handlePress = useCallback(...)
No getItemLayoutFlatList measures every item on scroll (expensive)Provide
getItemLayout
for fixed-height items:
(data, index) => ({ length: 80, offset: 80 * index, index })
FlashListFlatList is good, FlashList is better for large lists
@shopify/flash-list
— drop-in replacement, recycling architecture
Large images in listsFull-res images decoded on main threadUse
expo-image
with placeholder + transition, specify dimensions
列表是React Native中排名第一的性能问题。卡顿的滚动会彻底毁掉应用体验。
模式问题修复方案
用ScrollView展示数据
<ScrollView>
会一次性渲染所有列表项
使用
<FlatList>
<FlashList>
——虚拟列表,仅渲染可见项
缺少keyExtractorFlatList未配置
keyExtractor
→ 不必要的重渲染
设置
keyExtractor={(item) => item.id}
——为每个列表项提供稳定的唯一键
复杂的renderItemrenderItem中的昂贵组件会在每次滚动时重渲染
React.memo
包裹,提取为独立组件
renderItem中的内联函数
renderItem={({ item }) => <Row onPress={() => nav(item.id)} />}
提取处理函数:
const handlePress = useCallback(...)
未配置getItemLayoutFlatList在滚动时会测量每个列表项(性能开销大)为固定高度的列表项提供
getItemLayout
(data, index) => ({ length: 80, offset: 80 * index, index })
FlashListFlatList表现不错,但FlashList在处理大型列表时更出色
@shopify/flash-list
——可直接替换FlatList,采用回收式架构
列表中包含大尺寸图片全分辨率图片会在主线程解码使用
expo-image
,配合占位图与过渡效果,并指定尺寸

FlatList Checklist

FlatList 检查清单

Every FlatList should have:
tsx
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}           // Memoised component
  getItemLayout={getItemLayout}     // If items are fixed height
  initialNumToRender={10}           // Don't render 100 items on mount
  maxToRenderPerBatch={10}          // Batch size for off-screen rendering
  windowSize={5}                    // How many screens to keep in memory
  removeClippedSubviews={true}      // Unmount off-screen items (Android)
/>
每个FlatList都应配置以下属性:
tsx
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}           // 已记忆化的组件
  getItemLayout={getItemLayout}     // 若列表项高度固定
  initialNumToRender={10}           // 初始化时不要渲染100个项
  maxToRenderPerBatch={10}          // 屏幕外渲染的批次大小
  windowSize={5}                    // 内存中保留的屏幕数量
  removeClippedSubviews={true}      // 卸载屏幕外的列表项(Android)
/>

2. Animations (HIGH)

2. 动画(HIGH)

Native animations run on the UI thread. JS animations block the JS thread and cause jank.
PatternProblemFix
Animated API for complex animations
Animated
runs on JS thread, blocks interactions
Use
react-native-reanimated
— runs on UI thread
Layout animationItem appears/disappears with no transition
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
Shared element transitionsNavigate between screens, element teleports
react-native-reanimated
shared transitions or
expo-router
shared elements
Gesture + animationDrag/swipe feels laggy
react-native-gesture-handler
+
reanimated
worklets — all on UI thread
Measuring layout
onLayout
fires too late, causes flash
Use
useAnimatedStyle
with shared values for instant response
原生动画在UI线程运行。JS动画会阻塞JS线程并导致卡顿。
模式问题修复方案
用Animated API实现复杂动画
Animated
在JS线程运行,会阻塞交互
使用
react-native-reanimated
——在UI线程运行
布局动画列表项出现/消失时无过渡效果
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
共享元素过渡页面跳转时,元素突兀地移动使用
react-native-reanimated
的共享过渡或
expo-router
的共享元素功能
手势+动画拖拽/滑动操作感觉延迟
react-native-gesture-handler
+
reanimated
工作流——全部在UI线程执行
测量布局
onLayout
触发过晚,导致闪烁
使用
useAnimatedStyle
配合共享值以获得即时响应

Reanimated Basics

Reanimated 基础示例

tsx
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';

function AnimatedBox() {
  const offset = useSharedValue(0);
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: withSpring(offset.value) }],
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.box, style]} />
    </GestureDetector>
  );
}
tsx
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';

function AnimatedBox() {
  const offset = useSharedValue(0);
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: withSpring(offset.value) }],
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.box, style]} />
    </GestureDetector>
  );
}

3. Navigation (HIGH)

3. 导航(HIGH)

PatternProblemFix
Expo RouterFile-based routing (like Next.js) for React Native
app/
directory with
_layout.tsx
files. Preferred for new Expo projects.
Heavy screens on stackEvery screen stays mounted in the stackUse
unmountOnBlur: true
for screens that don't need to persist
Deep linkingApp doesn't respond to URLsExpo Router handles this automatically. For bare RN:
Linking
API config
Tab badge updatesBadge count doesn't update when tab is focusedUse
useIsFocused()
or refetch on focus:
useFocusEffect(useCallback(...))
Navigation state persistenceApp loses position on background/kill
onStateChange
+
initialState
with AsyncStorage
模式问题修复方案
Expo Router类Next.js的文件式路由,适用于React Native采用
app/
目录结构,搭配
_layout.tsx
文件。为新Expo项目的首选方案。
栈中包含重负载页面栈中的每个页面都会保持挂载状态对无需持久化的页面设置
unmountOnBlur: true
深度链接应用对URL无响应Expo Router会自动处理。对于裸RN项目:配置
Linking
API
Tab徽章更新切换到标签页时徽章计数未更新使用
useIsFocused()
或在页面获取焦点时重新获取数据:
useFocusEffect(useCallback(...))
导航状态持久化应用进入后台/被杀死后丢失当前位置
onStateChange
+ 配合AsyncStorage设置
initialState

Expo Router Structure

Expo Router 目录结构

app/
├── _layout.tsx          # Root layout (tab navigator)
├── index.tsx            # Home tab
├── (tabs)/
│   ├── _layout.tsx      # Tab bar config
│   ├── home.tsx
│   ├── search.tsx
│   └── profile.tsx
├── [id].tsx             # Dynamic route
└── modal.tsx            # Modal route
app/
├── _layout.tsx          # 根布局(标签导航器)
├── index.tsx            # 首页标签
├── (tabs)/
│   ├── _layout.tsx      # 标签栏配置
│   ├── home.tsx
│   ├── search.tsx
│   └── profile.tsx
├── [id].tsx             # 动态路由
└── modal.tsx            # 模态路由

4. UI Patterns (HIGH)

4. UI模式(HIGH)

PatternProblemFix
Safe areaContent under notch or home indicator
<SafeAreaView>
or
useSafeAreaInsets()
from
react-native-safe-area-context
Keyboard avoidanceForm fields hidden behind keyboard
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
Platform-specific codeiOS and Android need different behaviour
Platform.select({ ios: ..., android: ... })
or
.ios.tsx
/
.android.tsx
files
Status barStatus bar overlaps content or wrong colour
<StatusBar style="auto" />
from
expo-status-bar
in root layout
Touch targetsButtons too small to tapMinimum 44x44pt. Use
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
Haptic feedbackTaps feel dead
expo-haptics
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
on important actions
模式问题修复方案
安全区域内容被刘海或Home指示器遮挡使用
<SafeAreaView>
react-native-safe-area-context
中的
useSafeAreaInsets()
键盘避让表单字段被键盘遮挡
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
平台专属代码iOS和Android需要不同的行为
Platform.select({ ios: ..., android: ... })
或使用
.ios.tsx
/
.android.tsx
文件
状态栏状态栏与内容重叠或颜色错误在根布局中使用
expo-status-bar
<StatusBar style="auto" />
触摸目标按钮过小难以点击最小尺寸为44x44pt。使用
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
触觉反馈点击操作无反馈感
expo-haptics
——在重要操作时调用
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)

5. Images and Media (MEDIUM)

5. 图片与媒体(MEDIUM)

PatternProblemFix
Image component
<Image>
from react-native is basic
Use
expo-image
— caching, placeholder, transition, blurhash
Remote images without dimensionsLayout shift when image loadsAlways specify
width
and
height
, or use
aspectRatio
Large imagesOOM crashes on AndroidResize server-side or use
expo-image
which handles memory
SVGSVG support isn't native
react-native-svg
+
react-native-svg-transformer
for SVG imports
VideoVideo playback
expo-av
or
expo-video
(newer API)
模式问题修复方案
Image组件react-native的
<Image>
功能基础
使用
expo-image
——支持缓存、占位图、过渡效果、blurhash
未指定尺寸的远程图片图片加载时布局偏移始终指定
width
height
,或使用
aspectRatio
大尺寸图片Android上出现内存不足崩溃在服务端调整尺寸,或使用
expo-image
(它会处理内存问题)
SVG原生不支持SVG
react-native-svg
+
react-native-svg-transformer
以支持导入SVG文件
视频播放实现视频播放功能使用
expo-av
expo-video
(更新的API)

6. State and Data (MEDIUM)

6. 状态与数据(MEDIUM)

PatternProblemFix
AsyncStorage for complex dataJSON parse/stringify on every readUse MMKV (
react-native-mmkv
) — 30x faster than AsyncStorage
Global stateRedux/MobX boilerplate for simple stateZustand — minimal, works great with React Native
Server stateManual fetch + loading + error + cacheTanStack Query — same as web, works in React Native
Offline firstApp unusable without networkTanStack Query
persistQueryClient
+ MMKV, or WatermelonDB for complex offline
Deep state updatesSpread operator hell for nested objectsImmer via Zustand:
set(produce(state => { state.user.name = 'new' }))
模式问题修复方案
用AsyncStorage存储复杂数据每次读取都要进行JSON解析/序列化使用MMKV(
react-native-mmkv
)——比AsyncStorage快30倍
全局状态Redux/MobX对于简单状态来说样板代码过多Zustand——轻量,非常适合React Native
服务端状态手动处理请求+加载+错误+缓存TanStack Query——与Web端用法一致,适用于React Native
离线优先无网络时应用无法使用TanStack Query的
persistQueryClient
+ MMKV,或复杂离线场景使用WatermelonDB
深度状态更新嵌套对象的展开运算符地狱通过Zustand使用Immer:
set(produce(state => { state.user.name = 'new' }))

7. Expo Workflow (MEDIUM)

7. Expo 工作流(MEDIUM)

PatternWhenHow
Development buildNeed native modules
npx expo run:ios
or
eas build --profile development
Expo GoQuick prototyping, no native modules
npx expo start
— scan QR code
EAS BuildCI/CD, app store builds
eas build --platform ios --profile production
EAS UpdateHot fix without app store review
eas update --branch production --message "Fix bug"
Config pluginsModify native config without ejecting
app.config.ts
with
expo-build-properties
or custom config plugin
Environment variablesDifferent configs per build
eas.json
build profiles +
expo-constants
模式适用场景实现方式
开发构建需要使用原生模块时
npx expo run:ios
eas build --profile development
Expo Go快速原型开发,无需原生模块
npx expo start
—— 扫描二维码
EAS BuildCI/CD、应用商店构建
eas build --platform ios --profile production
EAS Update无需应用商店审核的热修复
eas update --branch production --message "Fix bug"
配置插件无需 eject 即可修改原生配置
app.config.ts
中使用
expo-build-properties
或自定义配置插件
环境变量不同构建使用不同配置
eas.json
构建配置 +
expo-constants

New Project Setup

新项目搭建步骤

bash
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context
bash
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context

8. Testing (LOW-MEDIUM)

8. 测试(LOW-MEDIUM)

ToolForSetup
JestUnit tests, hook testsIncluded with Expo by default
React Native Testing LibraryComponent tests
@testing-library/react-native
DetoxE2E tests on real devices/simulators
detox
— Wix's testing framework
MaestroE2E with YAML flows
maestro test flow.yaml
— simpler than Detox
工具用途搭建方式
Jest单元测试、Hook测试Expo默认已包含
React Native Testing Library组件测试安装
@testing-library/react-native
Detox真机/模拟器上的端到端测试
detox
—— Wix的测试框架
Maestro基于YAML流程的端到端测试
maestro test flow.yaml
—— 比Detox更简单

Common Gotchas

常见陷阱

GotchaFix
Metro bundler cache
npx expo start --clear
Pod install issues (iOS)
cd ios && pod install --repo-update
Reanimated not workingMust be first import:
import 'react-native-reanimated'
in root
Expo SDK upgrade
npx expo install --fix
after updating SDK version
Android build failsCheck
gradle.properties
for memory:
org.gradle.jvmargs=-Xmx4g
iOS simulator slowUse physical device for performance testing — simulator doesn't reflect real perf
陷阱修复方案
Metro打包器缓存
npx expo start --clear
iOS Pod安装问题
cd ios && pod install --repo-update
Reanimated无法工作必须作为第一个导入项:在根文件中
import 'react-native-reanimated'
Expo SDK升级更新SDK版本后执行
npx expo install --fix
Android构建失败检查
gradle.properties
中的内存配置:
org.gradle.jvmargs=-Xmx4g
iOS模拟器运行缓慢使用真机进行性能测试——模拟器无法反映真实性能