mcpField
R
react-native-advanced

react-native-advanced

github.com/trancong12102开发748 次安装5 周前更新
代码仓库

关于 react-native-advanced

面向 React Native 和 Expo 开发者的进阶技能,整理 TanStack、XState 与 Zustand 生态的原生端模式、集成要点和常见陷阱。

主要功能

  1. 01React Query 的原生端集成规则
  2. 02Expo Router 导航与认证模式
  3. 03FlashList 无限滚动实践
  4. 04MMKV 持久化与加密存储
  5. 05动画、手势与推送更新指南

适用场景

  1. 01配置 AppState 与 NetInfo 查询管理器
  2. 02为 Expo Router 屏幕预取和刷新数据
  3. 03构建基于 FlashList 的分页列表
  4. 04处理动画、通知和 OTA 更新

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trancong12102/agentskills --skill react-native-advanced

安装后在 Claude Code 中输入/react-native-advanced确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
animations.md
bottom-sheet.md
expo-essentials.md
expo-router.md
lists.md
notifications.md
react-query-rn.md
testing-rn.md
zustand-rn.md

详细介绍

React Native 进阶:Expo + TanStack/XState/Zustand 生态

面向基于 TanStack 生态、XState 和 Zustand 构建的应用的 React Native 和 Expo 模式。本技能扩展了 react-advanced(核心跨平台模式)。请先阅读该技能,了解 React Query、XState、Zustand、Zod、TanStack Form 和 TanStack Table 的约定。


RN 架构

与 Web 相比,各库在 React Native 中的对应关系有所不同:

Web 库RN 对应项主要区别
TanStack RouterExpo Router原生端没有路由加载器,采用基于文件的导航
TanStack Start—原生端没有 SSR/服务端函数
TanStack VirtualFlashList原生视图回收,而非 DOM 虚拟化
localStorageMMKV同步、在原生线程运行,速度快 30 倍
window eventsAppState/NetInfo需要手动接入焦点/在线管理器
CSS animationsReanimatedUI 线程 worklet、CSS 过渡(v4)
DOM eventsGesture Handler手势组合 API、UI 线程回调
<img>expo-imageSDWebImage/Glide、blurhash、磁盘缓存
Web Push APIexpo-notificationsFCM/APNs、渠道、后台任务
Service Workersexpo-updatesOTA 更新、分阶段发布、紧急回滚

跨平台库(在 Web 和 RN 上使用相同的 API): React Query、XState、Zustand、Zod、TanStack Form、TanStack Table


必需的设置

两项集成必须配置,否则 React Query 的自动重新获取和离线处理在 React Native 中无法正常工作:

  • focusManager — 将 AppState 接入 focusManager.setFocused(),使应用返回前台时 refetchOnWindowFocus 能够正常工作。若未配置,默认行为会被静默忽略。
  • onlineManager — 将 NetInfo 接入 onlineManager.setOnline(),使查询在网络连接状态变化时暂停/恢复。

在 QueryClientProvider 中的根 _layout.tsx 里,各调用一次这两个钩子。

完整实现请参阅 references/react-query-rn.md。


无路由加载器的数据获取

Expo Router 没有原生路由加载器(数据加载器仅适用于 Web,且仍处于 alpha 阶段)。模式是:在用户交互时预取数据,并在目标屏幕中使用。

规则:

  • 在 router.push 之前调用 prefetchQuery,不要使用 await — 等待会导致导航变慢。
  • 使用 useFocusEffect(来自 expo-router),在屏幕重新获得焦点时使过期数据失效。屏幕会保持挂载,因此导航返回时 useEffect 不会重新运行。
  • 使用 invalidateQueries(遵循 staleTime),而不是 refetch()(总会重新获取)。

预取和 useRefreshOnFocus 模式请参阅 references/react-query-rn.md。


导航 + 身份验证

Stack.Protected(Expo Router v5+,推荐)
// app/_layout.tsx
export default function RootLayout() {
  const session = useAuthStore((s) => s.session)

  return (
    <Stack>
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
      </Stack.Protected>
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
      </Stack.Protected>
    </Stack>
  )
}

当 session 发生变化时,Expo Router 会自动重定向并清理历史记录。对于复杂的身份验证流程(令牌检查、刷新、错误恢复),使用 XState 管理身份验证状态,并为 guard 属性派生一个布尔值。

XState 身份验证变体和 Zustand 身份验证存储模式请参阅 references/expo-router.md。


列表:FlashList + React Query

FlashList 在 RN 中取代 TanStack Virtual——它使用原生视图复用,而不是基于 DOM 的绝对定位。

规则:

  • onEndReached 中的 !isFetchingNextPage 守卫至关重要——FlashList 可能会在短时间内多次触发 onEndReached,导致重复获取。
  • 使用 useMemo 记忆扁平化后的 items 数组——data.pages.flatMap() 每次渲染都会创建一个新的引用。
  • 使用 useCallback 稳定 handleEndReached。
  • getNextPageParam 必须返回 undefined(而不是 null),以表示没有下一页。

完整的无限滚动模式、FlashList v2 变更和性能调优,请参阅 references/lists.md。


RN 中的 TanStack Form

TanStack Form 是无头框架——不依赖 DOM,也不需要适配器。与 Web 的主要区别:

  • TextInput 使用 onChangeText(直接传入字符串),而不是 onChange(事件对象)。 将 field.handleChange 直接绑定到 onChangeText。
  • 对于数值字段,在调用处进行转换: onChangeText={(val) => field.handleChange(val === '' ? null : Number(val))}。
  • 使用 keyboardShouldPersistTaps="handled" 将表单包在 ScrollView 中——否则,首次点击 Submit 时会关闭键盘,而不会触发按下事件。

使用 MMKV 的 Zustand Persist

MMKV 是同步的,并且运行在原生线程上——不存在异步水合间隙。在模块级别创建 MMKV 实例(绝不要在组件内部创建)。StateStorage 适配器在键不存在时必须返回 null:mmkv.getString(name) ?? null。

对于敏感数据,请使用带有 encryptionKey 的加密 MMKV 实例。有关混合模式(硬件保护密钥 + 加密 MMKV),请参阅 references/expo-essentials.md。

完整的适配器、存储创建、加密存储、水合时序和原生存储模式,请参阅 references/zustand-rn.md。


动画与手势

Reanimated 通过 worklet 在 UI 线程 上运行动画。Gesture Handler 会将触摸事件路由到同一线程。Reanimated 4 新增了 CSS 风格的声明式过渡。

关键规则
  • 替换整个值:使用 sv.value = { x: 50 },不要使用 sv.value.x = 50(会破坏响应性)
  • 不要解构:const { x } = sv.value 会创建普通数字,而不是响应式值
  • 不要在渲染期间读取:读取共享值属于副作用,违反 React 规则
  • 共享值不会触发重新渲染:如果需要 React 做出响应,请维护单独的状态
  • GestureHandlerRootView 必须以 style={{ flex: 1 }} 包裹应用根节点
  • Android 模态框需要自己的 GestureHandlerRootView(位于原生根视图之外)
线程处理概要

共享值位于 UI 线程。在 JS 线程上读取 .value 会产生阻塞式桥接调用——绝不要在热点路径中这样做。手势回调和 useAnimatedStyle 在 UI 线程上运行。使用 runOnJS(Reanimated 3)或 scheduleOnRN(Reanimated 4)调用 JS 函数。

声明式与命令式

对于由状态驱动的样式变更(切换颜色、不透明度、尺寸),使用 CSS 过渡(Reanimated 4)。对于挂载/卸载,使用 布局动画(entering/exiting 属性)。对于由手势驱动的动画和命令式动画链,使用 worklet + 共享值。

详细模式、Reanimated 4 API 变更、手势组合和 CSS 过渡,请参阅 references/animations.md。


底部弹出层

@lodev09/react-native-true-sheet——由 UISheetPresentationController(iOS)和 BottomSheetDialog(Android)支持的原生底部弹出层。不依赖 Reanimated。仅支持新架构。

关键规则
  • 仅使用命令式控制——使用 ref.present()/dismiss()/resize(),不要使用状态属性。
  • 最多使用 3 个停靠位置,按从小到大排序。内容自适应时使用 'auto'。
  • 不要将 scrollable 与 'auto' 停靠位置组合使用——两者会冲突。请使用固定停靠位置。
  • 不要在工作表内容上使用 flex: 1——这会使高度塌缩为零。请使用 flexGrow 或固定高度。
  • 卸载前务必调用 dismiss()——原生工作表的生命周期会超过 React 组件。
  • 可使用标准 TextInput——无需特殊的键盘组件(原生会处理)。
  • 标准 ScrollView/FlashList 可正常使用,搭配 scrollable + nestedScrollEnabled 即可——v3 会自动检测(最多深入 2 层)。

完整用法、平台差异和 Expo Router 集成,请参阅 references/bottom-sheet.md。


推送通知

expo-notifications 通过 Expo 的推送服务处理 FCM(Android)和 APNs(iOS)。

SDK 53 重大变更
  • Android 推送在 Expo Go 中无法使用——需要开发构建。
  • 必须在 app.json 的 plugins 数组中显式配置插件。
关键模式
// Must configure or foreground notifications are silently suppressed
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});
通知规则
  • 在 Android 13 及更高版本上,请求权限前必须先创建 Android 通知渠道。
  • 通知渠道不可变——创建后无法更改重要性或振动设置。
  • 在生产环境中轮询回执——票据只能确认 Expo 已收到请求。
  • 必须在模块作用域定义后台任务(不能在组件内部定义)。

有关权限处理、监听器和后台任务,请参阅 references/notifications.md。


OTA 更新

expo-updates 支持通过无线方式更新 JavaScript bundle,无需发布到应用商店。

更新检查模式
import * as Updates from "expo-updates";

// Run after first render, not during startup (can freeze UI on slow network)
const check = await Updates.checkForUpdateAsync();
if (check.isAvailable) {
  await Updates.fetchUpdateAsync();
  await Updates.reloadAsync();
}
紧急启动检测
if (Updates.isEmergencyLaunch) {
  // OTA caused a crash — app rolled back to embedded bundle
  // Log immediately to error tracking
}

务必对此进行监控——这是 OTA 更新在生产环境中的崩溃信号。

有关分阶段发布、运行时版本和响应式模式,请参阅 references/expo-essentials.md。


文件组织

结构:app/ 包含路由文件,每个路由段都有一个 _layout.tsx。路由组 (auth)/、(app)/、(tabs)/ 用于组织路由,不影响 URL。功能代码放在 app/ 之外:queries/(queryOptions 对象,而非钩子)、mutations/、machines/(纯 TS,不导入 React)、stores/(Zustand + MMKV 持久化)、hooks/、components/。


常见陷阱

  1. 未设置 focusManager/onlineManager — 未手动接入 AppState 和 NetInfo 时,refetchOnWindowFocus 和离线暂停都不会生效。

  2. 在 router.push 前等待 prefetchQuery 完成 — 会让导航显得缓慢。无需等待,直接触发预取,让 React Query 缓存为目标屏幕提供数据。

  3. 使用 useGlobalSearchParams 而不是 useLocalSearchParams — 全局参数会在每次导航事件时触发重新渲染。始终优先使用局部参数。

  4. 用 useEffect 在屏幕获得焦点时重新获取数据 — 导航返回时,useEffect 不会重新运行(屏幕仍处于挂载状态)。请使用 expo-router 中的 useFocusEffect。

  5. getNextPageParam 返回 null — 必须返回 undefined 才能表示没有下一页。将 API 返回的 null 转换为:lastPage.nextCursor ?? undefined。

  6. FlashList v2 需要 New Architecture — v2 是针对 Fabric 从头重写的版本。estimatedItemSize 已弃用(会被忽略)。overrideItemLayout 仅支持 span。

  7. MMKV 的 getString 返回 undefined — Zustand 的 StateStorage.getItem 在键不存在时必须返回 null。始终进行转换:?? null。

  8. 在组件内部创建 MMKV 实例 — 每次渲染都会创建新实例。请在模块级别声明。

  9. 表单 ScrollView 未设置 keyboardShouldPersistTaps — 第一次点击会关闭键盘,而不是按下 Submit 按钮。

  10. 在移动设备上使用默认的 retry: 3 — 网络不稳定时,指数退避的 3 次重试可能耗时 30 秒以上。对于时间敏感的 UI,可考虑设置 retry: 1。

  11. 修改共享值的属性 — sv.value.x = 50 会破坏响应性。必须替换整个值:sv.value = { x: 50, y: 0 }。

  12. GestureHandlerRootView 缺少 flex: 1 — 包装器的高度为零,手势看起来完全不起作用。

  13. TrueSheet 打开时将其卸载 — 原生表单不会自动关闭。从树中移除之前,务必调用 dismiss()。

  14. 在 TrueSheet 中将 auto detent 与 scrollable 搭配使用 — 自动调整尺寸与滚动固定会冲突。scrollable={true} 时请使用固定 detents。

  15. 未设置 setNotificationHandler — 所有前台通知都会被静默屏蔽,且不会报错。

  16. 慢速网络下调用 checkForUpdateAsync — 可能导致 Android UI 冻结数分钟。请在首次渲染后运行,并设置手动超时。

  17. 在 FlashList 中使用 expo-image 时未设置 recyclingKey — 如果不设置,回收的单元格会闪现上一个项目的图片。

  18. expo-secure-store 生物识别失效 — 生物识别信息变更时,设置了 requireAuthentication 的密钥将永久无法读取。除清除应用数据外,没有恢复方法。


参考文件

文件阅读时机
references/react-query-rn.md焦点/在线管理器、缓存持久化、预取
references/expo-router.md类型化路由、布局、模态框、身份验证、搜索参数
references/lists.mdFlashList + React Query、无限滚动、性能
references/zustand-rn.mdMMKV 持久化适配器、加密、水合模式
references/testing-rn.mdRNTL、测试 Query/Router/Form/XState、RN 中的 MSW
references/animations.mdReanimated、Gesture Handler 模式与注意事项
references/bottom-sheet.mdreact-native-true-sheet 设置、detents、Expo Router
references/notifications.mdexpo-notifications 权限、监听器、后台
references/expo-essentials.mdexpo-image、expo-secure-store、expo-haptics、expo-updates

评论

支持 Markdown