用途
react-native-patterns 面向使用 Expo 构建生产级 React Native 应用的开发场景,提供导航、状态、数据获取、列表、样式与原生 API 等方面的实践模式。它与 rules/react-native/ 规则集配套:规则说明要强制什么,本技能展示如何实现。
技能明确指出,文中提到的 NativeWind、Zustand/Jotai、TanStack Query 等库只是常见且成熟的示例选项,模式本身比具体包更重要,任何等价方案均可。校验使用 Zod,以与 ECC 现有的 typescript/ 规则保持一致。
适用前提
这些模式假定采用托管式 Expo 工作流(Expo Router、EAS、expo-* 模块),并运行在新架构之上(新架构是近期 Expo SDK 的默认,从 SDK 55+ 起为强制)。它们不假定浏览器 DOM——React Native 没有 <div>、没有地址栏,也没有 Web 的数据获取默认行为。
启用时机:构建或编辑 React Native / Expo 的屏幕、组件或导航;使用 Expo Router(基于文件的 app/ 目录)设置路由;决定状态应放在何处(服务器缓存、客户端 store、路由参数还是表单);用 TanStack Query 接入数据获取并用 Zod 校验响应;渲染长列表或重列表;选择或应用样式方案(NativeWind 或 StyleSheet);访问原生设备 API(摄像头、定位、通知)或安全存储;审查 RN 代码中的移动端特有问题。不应在此使用 Web/React-DOM 模式——把 URL 当状态、<div>、面向浏览器的 SWR 都不适用于 React Native。
主要能力
项目结构(Expo Router):使用 app/ 下的文件式路由,路由文件保持精简,只负责读取并校验参数,然后委托给位于 components/ 或 features/ 的屏幕组件。例如 app/ 下有 _layout.tsx(根 stack)、(tabs)/_layout.tsx(标签导航)、(tabs)/index.tsx(首页)、user/[id].tsx(动态路由),另有 components/ 和 features/user/UserProfile.tsx。
导航中校验路由参数:深链和动态路由会传入不可信字符串,使用前需用 Zod 校验。示例中在 app/user/[id].tsx 用 useLocalSearchParams 与 router,定义 Params = z.object({ id: z.string().uuid() }),解析失败则 router.replace('/not-found') 并返回 null。
状态分层:不要把服务器数据复制进客户端 store,每种关注点各有归属。服务器状态(远程数据)用服务器缓存库(TanStack Query、SWR);客户端/UI 状态用轻量 store(Zustand、Jotai)或 Context;路由/导航状态用 Expo Router 参数;表单状态用表单库(如 React Hook Form)+ schema 校验;密钥/token 用 expo-secure-store;非密钥持久化用 AsyncStorage / MMKV。在状态确实需要共享之前,优先使用局部 useState。
数据获取:使用服务器缓存库(TanStack Query、SWR)而非在 useEffect 中 fetch。在边界处校验,并从 schema 推断类型,显式处理加载、错误与空状态。示例中定义 zod 的 User,并用 useQuery 的 queryFn 调用 User.parse(await api.getUser(id));useUpdateEmail 用 useMutation 并在 onSuccess 中 invalidateQueries。
列表:虚拟化,绝不在 ScrollView 中 map 大数组。示例使用 FlatList,配 keyExtractor、memoized 的 renderItem、initialNumToRender={10}、windowSize={5};大列表或异构列表使用 FlashList(Shopify)。
样式:选定一个体系并保持一致。StyleSheet.create() 是框架原生方案;工具类库(如 NativeWind)是常见替代。切勿在热路径的 JSX 中内联构建样式对象。示例给出 NativeWind 的 className 写法与 StyleSheet 的 create 写法。
原生 API:把 Expo SDK 调用与订阅封装在 use* hook 中,而非写在 JSX 里,并始终清理。示例的 useCurrentLocation 用状态机跟踪 status(loading / denied / granted),并在 useEffect 的清理函数中将 active 置为 false 以忽略卸载后的过期结果。token 使用 expo-secure-store 的 setItemAsync / getItemAsync(Keychain / Keystore)。
使用方式
技能通过完整示例展示组合用法。一个完整屏幕(app/(tabs)/orders.tsx)演示“路由 → 查询 → 列表 → 状态”:用 zod 定义 OrderSchema 与 OrdersSchema,用 useOrders 通过 useQuery 校验并获取数据,用 memo 包裹 OrderRow,用 useCallback 稳定 renderItem,并分别处理 isLoading、isError(accessibilityRole="alert")、空数据以及带 onRefresh/refreshing 的 FlatList。
一个表单示例(EmailForm)演示 React Hook Form + Zod resolver:用 zodResolver(Schema) 与 defaultValues,通过 Controller 绑定 TextInput(autoCapitalize="none"、keyboardType="email-address"、accessibilityLabel),错误用 Text 配 accessibilityRole="alert" 展示,按钮触发 handleSubmit(onSubmit)。
反模式(技能列出的错误做法与正确做法)
- 在 ScrollView 中 map 大数组(无虚拟化、卡顿、高内存)→ 使用 FlatList / FlashList。
- 把服务器数据复制进客户端 store(两个事实来源、数据陈旧)→ 由 useQuery 拥有服务器状态,按需派生。
- 把 token 放在 AsyncStorage(未加密)→ 使用 expo-secure-store。
- 信任深链参数(const { id } = useLocalSearchParams(); fetchUser(id))→ 使用前用 Zod 校验。
- 在热路径上每次渲染重建内联样式对象 → 模块作用域的 StyleSheet.create,或 NativeWind className。
- 在打包产物中携带真实密钥(const STRIPE_SECRET = 'sk_live_...')→ 特权调用保留在服务端,只发布受后端规则保护的公开密钥。
最佳实践
保持路由文件精简,把逻辑放在屏幕组件与 use* hook 中;用 Zod 校验所有外部输入(API 响应、路由参数、推送负载);让 TanStack Query 拥有服务器状态,客户端 store 保持小;始终渲染加载、错误与空状态,绝不只给一个没有兜底的 spinner;虚拟化列表、memoize renderItem、提供稳定的 keyExtractor;动画使用 react-native-reanimated(UI 线程),避免 JS 线程上做重活;token 存于 expo-secure-store,绝不信任客户端做授权;从一开始就尊重安全区域、Dynamic Type 与无障碍角色/标签;发布前确认每个原生依赖与新架构的兼容性。
相关技能
frontend-patterns 为 React/Next.js(Web)模式,对共享的 React 概念有用,但属于 DOM 专有;coding-standards 为适用于 RN 代码的 TypeScript/JavaScript 惯用法;tdd-workflow、e2e-testing 为测试流程(RN 使用 Jest + React Native Testing Library,Maestro/Detox);security-review 为通用安全检查清单,可补充上述 RN 打包产物/密钥指南。
限制
该技能明确不适用于 Web/React-DOM 模式,URL 即状态、<div>、面向浏览器的 SWR 等做法均不适用。其余如具体版本支持范围、收费与价格信息,资料中暂未提供。