Communitygithub.com

React Native / Expo 模式

React Native 和 Expo 应用模式 —— Expo Router 导航、状态分离(服务端/客户端/路由/表单)、基于 TanStack Query 和 Zod 的数据获取、高性能列表、NativeWind/StyleSheet 样式、原生 API 和安全存储。适用于构建或编辑 React Native / Expo 屏幕、组件、导航或数据层时使用。

Expo 模式 是什么?

该技能提供了一套经过实践验证的模式,用于构建生产级的 React Native 应用,特别是使用 Expo 管理的工作流。它全面覆盖了导航(Expo Router)、状态管理(明确区分服务端缓存、客户端状态和路由参数)、数据获取(推荐 TanStack Query 配合 Zod 进行类型安全验证)、高性能列表渲染(FlatList/FlashList)、样式系统选择(NativeWind 或 StyleSheet)、原生 API 调用(封装为自定义 Hooks 并清理副作用)以及安全令牌存储(expo-secure-store)。该技能特别强调保持路由文件精简,将业务逻辑下沉到组件和 Hooks 中;在架构层面明确反对将服务端数据复制到客户端 store 中,以此避免数据不一致问题。它适用于 React Native 开发者、Expo 框架用户以及使用 Expo Router 进行路由管理的团队。技能内容以详细代码示例和明确的反模式对比呈现,帮助开发者避免常见陷阱,例如在 ScrollView 中直接映射大数组、内联样式对象导致的性能问题以及忽略深度链接参数验证。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/react-native-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Expo 模式 是做什么的?

使用 Expo 构建生产级 React Native 应用的实用模式。涵盖导航、状态、数据获取、列表、样式和原生 API。与 rules/react-native/ 规则集配合使用:规则规定要执行什么,本技能展示如何执行。

下面列出的库(NativeWind、Zustand/Jotai、TanStack Query)是常见且成熟的选项,仅作示例说明——模式比具体包更重要,任何等效的库都可以。Zod 用于验证,以与 ECC 现有的 typescript/ 规则保持一致。

这些模式假设在新型架构(最近 Expo SDK 中的默认架构,从 SDK 55+ 开始是必需的)上使用托管的 Expo 工作流(Expo Router、EAS、expo-* 模块)。它们不假设浏览器 DOM——React Native 没有 <div>、没有 URL 栏,也没有 Web 数据获取的默认值。

何时激活

在以下情况下使用此技能:

  • 构建或编辑 React Native / Expo 屏幕、组件或导航
  • 使用 Expo Router(基于文件的 app/ 目录)设置路由
  • 决定状态归属(服务端缓存 vs 客户端存储 vs 路由参数 vs 表单)
  • 使用 TanStack Query 连接数据获取并使用 Zod 验证响应
  • 渲染长列表或大型列表
  • 选择或应用样式方法(NativeWind 或 StyleSheet)
  • 访问原生设备 API(相机、位置、通知)或安全存储
  • 审查 RN 代码中特定于移动端的问题

不要在此处使用 Web/React-DOM 模式——URL 即状态、<div> 和 SWR-for-browser 不适用于 React Native。

核心概念

项目结构(Expo Router)

app/ 下的基于文件的路由。保持路由文件精简:它们读取并验证参数,然后委托给位于 components/ 或 features/ 中的屏幕组件。

app/
  _layout.tsx          # 根栈导航
  (tabs)/
    _layout.tsx        # 标签导航器
    index.tsx          # 主页
  user/[id].tsx        # 动态路由
components/
features/
  user/UserProfile.tsx

导航:验证路由参数

深度链接和动态路由传递不可信的字符串。在使用前用 Zod 进行验证。

// app/user/[id].tsx
import { useLocalSearchParams, router } from 'expo-router'
import { z } from 'zod'
import { UserProfile } from '@/features/user/UserProfile'

const Params = z.object({ id: z.string().uuid() })

export default function UserRoute() {
  const parsed = Params.safeParse(useLocalSearchParams())
  if (!parsed.success) {
    router.replace('/not-found')
    return null
  }
  return <UserProfile userId={parsed.data.id} />
}

状态:保持关注点分离

不要将服务端数据复制到客户端 store 中。每个关注点都有自己的归属。

关注点常用选择
服务端状态(远程数据)服务端缓存库(TanStack Query, SWR)
客户端/UI 状态轻量级 store(Zustand, Jotai)或 Context
路由/导航状态Expo Router 参数
表单状态表单库(例如 React Hook Form)+ 模式验证
密钥/令牌expo-secure-store
非密钥持久化AsyncStorage / MMKV

在状态真正需要共享之前,优先使用本地的 useState。

数据获取:缓存库 + Zod

使用服务端缓存库(TanStack Query, SWR)而不是 useEffect 中的 fetch。在边界进行验证并根据模式推断类型。显式处理加载、错误和空状态。(示例使用 TanStack Query。)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { z } from 'zod'

const User = z.object({ id: z.string(), email: z.string().email() })
type User = z.infer<typeof User>

export function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: async (): Promise<User> => User.parse(await api.getUser(id)),
  })
}

export function useUpdateEmail(id: string) {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: (email: string) => api.updateEmail(id, email),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['user', id] }),
  })
}

列表:虚拟化,绝不在 ScrollView 中映射大数组

import { FlatList } from 'react-native'

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}          // 已记忆化
  initialNumToRender={10}
  windowSize={5}
/>

对于大型或异构列表,使用 FlashList(Shopify)。

样式:选择一个系统

StyleSheet.create() 是框架原生选项;实用类库(例如 NativeWind)是常见的替代方案。选择一个并保持一致。绝不要在热路径上的 JSX 中内联构建样式对象。

// NativeWind
<View className="p-4 rounded-2xl bg-white">
  <Text className="text-base font-semibold">你好</Text>
</View>

// StyleSheet
const styles = StyleSheet.create({ card: { padding: 16, borderRadius: 16, backgroundColor: '#fff' } })
<View style={styles.card}>...</View>

原生 API:封装到 hooks 中,清理副作用

将 Expo SDK 调用和订阅放在 use* hooks 内部,而不是 JSX 中。始终进行清理。

import { useEffect, useState } from 'react'
import * as Location from 'expo-location'

type LocationState =
  | { status: 'loading' }
  | { status: 'denied' }
  | { status: 'granted'; coords: Location.LocationObjectCoords }

export function useCurrentLocation() {
  // 跟踪状态,而不仅仅是坐标——这样 UI 可以区分“仍在加载”
  // 和“权限被拒绝”,并显示可操作的消息。
  const [state, setState] = useState<LocationState>({ status: 'loading' })

  useEffect(() => {
    let active = true
    ;(async () => {
      const { status } = await Location.requestForegroundPermissionsAsync()
      if (status !== 'granted') {
        if (active) setState({ status: 'denied' })
        return
      }
      const pos = await Location.getCurrentPositionAsync({})
      if (active) setState({ status: 'granted', coords: pos.coords })
    })()
    return () => { active = false }   // 卸载后忽略过期结果
  }, [])

  return state
}

令牌的安全存储

import * as SecureStore from 'expo-secure-store'

await SecureStore.setItemAsync('auth_token', token)   // Keychain / Keystore
const token = await SecureStore.getItemAsync('auth_token')

代码示例

完整屏幕:路由 → 查询 → 列表 → 状态

// app/(tabs)/orders.tsx
import { memo, useCallback } from 'react'
import { FlatList, Text, View } from 'react-native'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'

const OrderSchema = z.object({ id: z.string(), total: z.number(), status: z.string() })
const OrdersSchema = z.array(OrderSchema)
type Order = z.infer<typeof OrderSchema>

function useOrders() {
  return useQuery({
    queryKey: ['orders'],
    queryFn: async () => OrdersSchema.parse(await api.listOrders()),
  })
}

// 已记忆化,以便其引用在渲染之间保持稳定(参见列表指南)。
const OrderRow = memo(function OrderRow({ item }: { item: Order }) {
  return (
    <View className="px-4 py-3 border-b border-neutral-200">
      <Text className="font-medium">#{item.id}</Text>
      <Text className="text-neutral-500">{item.status} · ${item.total}</Text>
    </View>
  )
})

export default function OrdersScreen() {
  const { data, isLoading, isError, refetch, isRefetching } = useOrders()
  const renderItem = useCallback(({ item }: { item: Order }) => <OrderRow item={item} />, [])

  if (isLoading) return <Centered><Text>加载中…</Text></Centered>
  if (isError) return <Centered><Text accessibilityRole="alert">无法加载订单。</Text></Centered>
  if (!data?.length) return <Centered><Text>尚无订单。</Text></Centered>

  return (
    <FlatList
      data={data}
      keyExtractor={(o) => o.id}
      onRefresh={refetch}
      refreshing={isRefetching}
      renderItem={renderItem}
    />
  )
}

表单:React Hook Form + Zod 解析器

import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { TextInput, Button, Text } from 'react-native'

const Schema = z.object({ email: z.string().email('无效的邮箱') })
type FormValues = z.infer<typeof Schema>

export function EmailForm({ onSubmit }: { onSubmit: (v: FormValues) => void }) {
  const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(Schema),
    defaultValues: { email: '' },
  })

  return (
    <>
      <Controller
        control={control}
        name="email"
        render={({ field: { value, onChange, onBlur } }) => (
          <TextInput
            value={value}
            onChangeText={onChange}
            onBlur={onBlur}
            autoCapitalize="none"
            keyboardType="email-address"
            accessibilityLabel="邮箱地址"
          />
        )}
      />
      {errors.email && <Text accessibilityRole="alert">{errors.email.message}</Text>}
      <Button title="保存" onPress={handleSubmit(onSubmit)} />
    </>
  )
}

反模式

// 错误:在 ScrollView 中映射大数组(无虚拟化,卡顿,高内存)
<ScrollView>{items.map((i) => <Row key={i.id} item={i} />)}</ScrollView>
// 正确:FlatList / FlashList

// 错误:将服务端数据复制到客户端 store 中(两个数据源,数据过期)
const useStore = create((set) => ({ users: [], setUsers: (u) => set({ users: u }) }))
useEffect(() => { getUsers().then(setUsers) }, [])
// 正确:useQuery 拥有服务端状态;按需派生出所需内容

// 错误:将令牌存储在 AsyncStorage 中(未加密)
await AsyncStorage.setItem('auth_token', token)
// 正确:expo-secure-store

// 错误:信任深度链接参数
const { id } = useLocalSearchParams(); fetchUser(id)
// 正确:在使用前用 Zod 验证

// 错误:在热路径上每次渲染都重新创建内联样式对象
<View style={{ padding: 16, backgroundColor: '#fff' }} />
// 正确:在模块作用域使用 StyleSheet.create,或使用 NativeWind className

// 错误:将真正的密钥打包到包中
const STRIPE_SECRET = 'sk_live_...'
// 正确:将特权调用保留在服务端;仅发送受后端规则保护的公钥

最佳实践

  • 保持路由文件精简;将逻辑放入屏幕组件和 use* hooks 中。
  • 使用 Zod 验证每个外部输入(API 响应、路由参数、推送负载)。
  • 让 TanStack Query 拥有服务端状态;保持客户端 store 小巧。
  • 始终渲染加载、错误和空状态——绝不只是显示一个没有后备的旋转器。
  • 虚拟化列表;记忆化 renderItem;提供稳定的 keyExtractor。
  • 使用 react-native-reanimated 进行动画(UI 线程);避免在 JS 线程上进行繁重工作。
  • 将令牌存储在 expo-secure-store 中;绝不要信任客户端进行授权。
  • 从开始就尊重安全区域、动态类型和可访问性角色/标签。
  • 在发布前确认每个原生依赖项与新型架构的兼容性。

相关技能

  • frontend-patterns —— React/Next.js(Web)模式;对于共享的 React 概念有用,但是特定于 DOM。
  • coding-standards —— 适用于 RN 代码的 TypeScript/JavaScript 惯用法。
  • tdd-workflow, e2e-testing —— 测试流程(使用 Jest + React Native Testing Library, Maestro/Detox for RN)。
  • security-review —— 通用安全检查清单,补充上述 RN 包/密钥指南。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能