Communitygithub.com

React Native 與 Expo 開發模式

React Native 與 Expo 應用程式開發模式 — Expo Router 導航、狀態分離(伺服器/客戶端/路由/表單)、使用 Zod 進行 TanStack Query 資料擷取、高效能列表、NativeWind/StyleSheet 樣式設計、原生 API 以及安全儲存。在建立或編輯 React Native / Expo 畫面、元件、導航或資料層時使用。

React Native 與 Expo 開發模式 是什麼?

此技能提供了一套實用且經過驗證的模式,用於使用 Expo 構建生產就緒的 React Native 應用程式。它涵蓋了關鍵領域,包括使用 Expo Router 進行基於檔案的路由、清晰的狀態管理分離(伺服器端、客戶端、路由、表單)、結合 Zod 使用 TanStack Query 進行類型的資料擷取、高效能列表(使用 FlatList 或 FlashList)、一致的樣式選擇(NativeWind 或 StyleSheet),以及安全地存取原生 API 和安全儲存。該技能適用於使用 Managed Expo 工作流程並採用新架構(預設從 SDK 55+ 開始)的專案。它同步了配套的 `rules/react-native/` 規則集,並在規則集強制執行「什麼」的同時,說明「如何」實施這些模式,特別強調驗證外部輸入、避免常見的反模式(如將伺服器資料複製到客戶端儲存庫中),以及正確處理載入和錯誤狀態。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

React Native / Expo 模式

用於使用 Expo 建構生產級 React Native 應用程式的實用模式。涵蓋導航、狀態、資料擷取、列表、樣式設計和原生 API。與 rules/react-native/ 規則集搭配使用:規則說明要強制執行什麼,此技能則展示如何做。

下方列出的程式庫(NativeWind、Zustand/Jotai、TanStack Query)是常見且成熟的選項,此處僅供示意 — 模式本身比特定套件更重要,任何等效的替代方案均可使用。Zod 用於驗證,以與 ECC 現有的 typescript/ 規則保持一致。

這些模式假設採用 Managed Expo 工作流程(Expo Router、EAS、expo-* 模組),並基於新架構(近期 Expo SDK 的預設值,從 SDK 55+ 起為強制要求)。它們不假設有瀏覽器 DOM — React Native 沒有 <div>、沒有網址列,也沒有網路資料擷取的預設行為。

何時啟用

請在以下情況使用此技能:

  • 建構或編輯 React Native / Expo 畫面、元件或導航時
  • 使用 Expo Router(基於檔案的 app/ 目錄)設定路由時
  • 決定狀態歸屬時(伺服器快取 vs 客戶端儲存 vs 路由參數 vs 表單)
  • 使用 TanStack Query 處理資料擷取並使用 Zod 驗證回應時
  • 渲染長列表或大型列表時
  • 選擇或應用樣式方案(NativeWind 或 StyleSheet)時
  • 存取原生裝置 API(相機、位置、通知)或安全儲存時
  • 評審 React Native 程式碼中的行動裝置特定問題時

請勿在此使用網頁/React-DOM 模式 — 將 URL 視為狀態、使用 <div> 以及適用於瀏覽器的 SWR,這些在 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} />
}

狀態:保持關注點分離

不要將伺服器資料複製到客戶端儲存中。每個關注點都有其專屬歸屬。

關注點常見選擇
伺服器狀態(遠端資料)伺服器快取函式庫(TanStack Query、SWR)
客戶端 / UI 狀態輕量級儲存(Zustand、Jotai)或 Context
路由 / 導航狀態Expo Router 參數
表單狀態表單函式庫(例如 React Hook Form)+ schema 驗證
機密 / 令牌expo-secure-store
非機密持久化AsyncStorage / MMKV

在狀態真正需要共享之前,優先使用本地的 useState。

資料擷取:快取函式庫 + Zod

使用伺服器快取函式庫(TanStack Query、SWR)而非在 useEffect 中進行 fetch。在邊界進行驗證並從 schema 推斷類型。明確處理載入、錯誤和空狀態。(範例使用 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 中對大型陣列進行 map

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">Hello</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 內部對大型陣列進行 map(無虛擬化,卡頓,高記憶體)
<ScrollView>{items.map((i) => <Row key={i.id} item={i} />)}</ScrollView>
// 正確:FlatList / FlashList

// 錯誤:將伺服器資料複製到客戶端儲存中(兩個真相來源,資料過時)
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

// 錯誤:將真實機密包含在 bundle 中
const STRIPE_SECRET = 'sk_live_...'
// 正確:將特權呼叫保留在伺服器端;僅傳送由後端規則保護的公開金鑰

最佳實踐

  • 保持路由檔案精簡;將邏輯放在畫面元件和 use* hooks 中。
  • 使用 Zod 驗證每個外部輸入(API 回應、路由參數、推播負載)。
  • 讓 TanStack Query 擁有伺服器狀態;保持客戶端儲存小型。
  • 始終渲染載入、錯誤和空狀態 — 絕不僅僅顯示 spinner 而沒有備用方案。
  • 虛擬化列表;將 renderItem 記憶化;提供穩定的 keyExtractor。
  • 使用 react-native-reanimated 進行動畫(UI 執行緒);避免在 JS 執行緒上進行繁重工作。
  • 將令牌儲存在 expo-secure-store 中;切勿信任客戶端進行授權。
  • 從一開始就尊重安全區域、Dynamic Type 以及無障礙角色/標籤。
  • 在發布前,確認每個原生依賴項都與新架構相容。

相關技能

  • frontend-patterns — React/Next.js(網頁)模式;對共享的 React 概念有用,但特定於 DOM。
  • coding-standards — 適用於 React Native 程式碼的 TypeScript/JavaScript 慣用寫法。
  • tdd-workflow、e2e-testing — 測試流程(針對 React Native 使用 Jest + React Native Testing Library、Maestro/Detox)。
  • security-review — 一般安全檢查清單,與上述 React Native bundle/機密指南相輔相成。

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.

相關技能