diff --git a/rules/README.md b/rules/README.md
index e4b69f73..0a9f48e4 100644
--- a/rules/README.md
+++ b/rules/README.md
@@ -1,4 +1,5 @@
# Rules
+
## Structure
Rules are organized into a **common** layer plus **language-specific** directories:
@@ -21,6 +22,7 @@ rules/
├── python/ # Python specific
├── golang/ # Go specific
├── web/ # Web and frontend specific
+├── react-native/ # React Native / Expo specific
├── swift/ # Swift specific
├── php/ # PHP specific
├── ruby/ # Ruby / Rails specific
@@ -43,6 +45,7 @@ rules/
./install.sh python
./install.sh golang
./install.sh web
+./install.sh react-native
./install.sh swift
./install.sh php
./install.sh ruby
@@ -79,6 +82,7 @@ cp -r rules/nuxt ~/.claude/rules/ecc/
cp -r rules/python ~/.claude/rules/ecc/
cp -r rules/golang ~/.claude/rules/ecc/
cp -r rules/web ~/.claude/rules/ecc/
+cp -r rules/react-native ~/.claude/rules/ecc/
cp -r rules/swift ~/.claude/rules/ecc/
cp -r rules/php ~/.claude/rules/ecc/
cp -r rules/ruby ~/.claude/rules/ecc/
@@ -100,7 +104,7 @@ cp -r rules/typescript .claude/rules/ecc/
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
- **Skills** (`skills/` directory) provide deep, actionable reference material for specific tasks (e.g., `python-patterns`, `golang-testing`).
-Language-specific rule files reference relevant skills where appropriate. Rules tell you *what* to do; skills tell you *how* to do it.
+Language-specific rule files reference relevant skills where appropriate. Rules tell you _what_ to do; skills tell you _how_ to do it.
## Adding a New Language
@@ -126,7 +130,7 @@ For non-language domains like `web/`, follow the same layered pattern when there
When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence).
- `rules/common/` defines universal defaults applicable to all projects.
-- `rules/golang/`, `rules/python/`, `rules/swift/`, `rules/php/`, `rules/typescript/`, etc. override those defaults where language idioms differ.
+- `rules/golang/`, `rules/python/`, `rules/swift/`, `rules/php/`, `rules/typescript/`, `rules/react-native/`, etc. override those defaults where language idioms differ.
### Example
diff --git a/rules/react-native/accessibility.md b/rules/react-native/accessibility.md
new file mode 100644
index 00000000..86c1f2b6
--- /dev/null
+++ b/rules/react-native/accessibility.md
@@ -0,0 +1,55 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Accessibility
+
+> Extends the ECC quality bar to accessibility (a11y). Treat a11y as a release requirement, not an afterthought.
+> Target: usable with screen readers (VoiceOver on iOS, TalkBack on Android) and at large font sizes.
+
+## Labeling
+
+- Every interactive element has an `accessibilityRole` and an `accessibilityLabel` (or readable child text).
+- Icon-only buttons MUST have an `accessibilityLabel` — there is no visible text for the reader to announce.
+- Use `accessibilityHint` only when the action is non-obvious; keep it short.
+- Group related elements with `accessible` on the container so they're announced as one unit when appropriate.
+
+```tsx
+
+
+
+```
+
+## State & Live Regions
+
+- Communicate state with `accessibilityState` (e.g. `{ disabled, selected, checked, expanded }`).
+- Announce async/transient changes (toasts, validation errors) via `accessibilityLiveRegion` (Android) and `AccessibilityInfo.announceForAccessibility` where needed.
+- Reflect loading/error/empty states in text the reader can reach — not just spinners or color.
+
+## Touch Targets & Layout
+
+- Minimum touch target ~44x44pt (iOS) / 48x48dp (Android); use `hitSlop` to enlarge small controls.
+- Respect Dynamic Type / font scaling — avoid fixed heights that clip scaled text; test at the largest accessibility font size.
+- Honor `prefers-reduced-motion` (`AccessibilityInfo.isReduceMotionEnabled`) — gate non-essential animation.
+
+## Color & Contrast
+
+- Do not convey meaning by color alone; pair with text, icon, or shape.
+- Meet WCAG AA contrast: 4.5:1 for body text, 3:1 for large text and meaningful UI/graphical elements.
+- Verify both light and dark themes.
+
+## Focus & Navigation
+
+- Logical focus order; move focus to new content (modals, screens) on open and restore on close.
+- Ensure custom components are reachable and operable by the screen reader, not just by touch.
+
+## Testing
+
+- Manually test with VoiceOver and TalkBack on real devices — automated checks do not catch everything.
+- In component tests, query by role/label (see testing.md) so a11y and tests reinforce each other.
+- Add a11y to the pre-release gate: key flows pass a screen-reader walkthrough.
diff --git a/rules/react-native/coding-style.md b/rules/react-native/coding-style.md
new file mode 100644
index 00000000..5de07cf7
--- /dev/null
+++ b/rules/react-native/coding-style.md
@@ -0,0 +1,71 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with React Native / Expo specific content.
+
+## Components
+
+- Define props with a named `interface` or `type`; do not use `React.FC`.
+- Keep screens thin: a screen composes hooks + presentational components, it does not hold heavy logic.
+- One component per file for anything reusable; co-locate small private subcomponents.
+- Prefer function components and hooks. No class components.
+
+```tsx
+interface AvatarProps {
+ uri: string
+ size?: number
+ onPress?: () => void
+}
+
+export function Avatar({ uri, size = 40, onPress }: AvatarProps) {
+ return (
+
+
+
+ )
+}
+```
+
+## Styling
+
+Pick ONE styling system per project and stay consistent. `StyleSheet.create()` is the framework-native option; utility-class libraries (e.g. NativeWind) are a common alternative. This rule is library-agnostic — what matters is consistency and avoiding inline allocations.
+
+- StyleSheet: define styles with `StyleSheet.create()` at module scope — never build style objects inline inside `render`/JSX on hot paths (it allocates on every render).
+- Utility-class approach: extract repeated class strings into shared constants or a variant helper.
+- Never hardcode raw colors, spacing, or font sizes scattered across files. Centralize design tokens (theme file or config).
+
+```tsx
+// WRONG: inline style object recreated every render
+
+
+// CORRECT (StyleSheet)
+const styles = StyleSheet.create({ card: { padding: 16, backgroundColor: '#fff' } })
+
+
+// CORRECT (NativeWind)
+
+```
+
+## Platform Differences
+
+- Use platform-specific files (`Component.ios.tsx`, `Component.android.tsx`) for substantial divergence.
+- Use `Platform.select()` / `Platform.OS` for small differences only.
+- Account for safe areas with `react-native-safe-area-context`; do not hardcode status bar / notch offsets.
+
+## Imports & Project Layout
+
+- Use the Expo/TS path alias (e.g. `@/components/...`) instead of long relative chains.
+- Organize by feature/domain, not by type. Keep files focused (200-400 lines typical, 800 max).
+
+## Logging
+
+- No `console.log` in shipped code. Use a logger and strip logs in production builds.
+- Surface user-facing errors through UI state, not console.
+
+## TypeScript
+
+All TypeScript rules from `rules/typescript/` apply (explicit types on public APIs, avoid `any`, Zod for validation, immutable updates). This file only adds RN-specific guidance on top.
diff --git a/rules/react-native/hooks.md b/rules/react-native/hooks.md
new file mode 100644
index 00000000..27759ed4
--- /dev/null
+++ b/rules/react-native/hooks.md
@@ -0,0 +1,28 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with React Native / Expo-specific automation guidance.
+
+These are recommended PostToolUse automations to keep RN/Expo code healthy. Wire them in your hook runtime (or run manually); adapt commands to your package manager.
+
+## Suggested PostToolUse checks (on edit of *.ts/*.tsx)
+
+- **Type check:** `tsc --noEmit` — catch type errors early.
+- **Lint:** `npx expo lint` (uses `eslint-config-expo`; flat config `eslint.config.js` is the default from SDK 53+).
+- **Format:** `prettier --write` on changed files.
+
+## Pre-release / periodic
+
+- `npx expo-doctor` — validates Expo/native dependency health and config.
+- `npx expo install --check` — keeps native deps aligned with the installed Expo SDK.
+- `npm audit` — dependency vulnerability scan.
+
+## Notes
+
+- Do not run heavy native builds inside fast edit hooks; keep edit-time hooks to typecheck/lint/format.
+- Reserve `eas build` / E2E for explicit commands or CI, not per-edit automation.
+- Keep these consistent with ECC hook runtime controls (`ECC_HOOK_PROFILE`, `ECC_DISABLED_HOOKS`).
diff --git a/rules/react-native/patterns.md b/rules/react-native/patterns.md
new file mode 100644
index 00000000..5ffef06a
--- /dev/null
+++ b/rules/react-native/patterns.md
@@ -0,0 +1,88 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with React Native / Expo specific patterns.
+> Note: Do NOT install the `web/` ruleset in a React Native project — those patterns assume the DOM (e.g. URL-as-state) and do not apply here.
+
+## Navigation (Expo Router)
+
+Expo Router is Expo's built-in, file-based router (`app/` directory); React Navigation is the established alternative. The examples below use Expo Router; the principles apply either way.
+
+- Keep route files (`app/**`) thin — they wire params + hooks to a screen component that lives in `components/` or `features/`.
+- Type route params; validate untrusted params (e.g. from deep links) with Zod before use.
+- Use typed navigation helpers (`useLocalSearchParams`, `Link`, `router.push`).
+- Centralize linking config; never trust deep-link params without validation.
+
+```tsx
+// app/user/[id].tsx
+import { useLocalSearchParams, router } from 'expo-router'
+import { z } from 'zod'
+
+const Params = z.object({ id: z.string().uuid() })
+
+export default function UserScreen() {
+ // Use safeParse, not parse: a malformed deep link would otherwise throw
+ // during render and crash the screen. Redirect instead of throwing.
+ const parsed = Params.safeParse(useLocalSearchParams())
+ if (!parsed.success) {
+ router.replace('/not-found')
+ return null
+ }
+ return
+}
+```
+
+## State Management
+
+The rule is to keep these concerns separate and not duplicate server data into client stores. The tools listed are common choices, not requirements — pick what fits your project.
+
+| Concern | Common choices |
+|---------|---------|
+| Server state | a server-cache library (TanStack Query, SWR) |
+| Client/UI state | a lightweight store (Zustand, Jotai) or Context |
+| Navigation/route state | Expo Router params (NOT a global store) |
+| Form state | a form library (e.g. React Hook Form) with schema validation |
+| Secure persistence | `expo-secure-store` |
+| Non-secure persistence | `AsyncStorage` / MMKV |
+
+- Derive values instead of storing redundant computed state.
+- Keep global client state minimal; prefer local `useState` until sharing is actually needed.
+
+## Data Fetching
+
+Use a server-cache library (TanStack Query, SWR) instead of ad-hoc fetch-in-`useEffect`. The examples use TanStack Query.
+
+- Route server reads through the cache (e.g. `useQuery`) and mutations through it (e.g. `useMutation`) with cache invalidation.
+- Validate API responses with Zod at the boundary; infer types from the schema. (Zod is already the validation default in ECC's `typescript/` rules.)
+- Handle the three states explicitly in UI: loading, error, empty.
+- Use optimistic updates for fast interactions: snapshot, apply, roll back on failure with visible feedback.
+- Fetch independent data in parallel; avoid request waterfalls between parent and child.
+
+```tsx
+function useUser(id: string) {
+ return useQuery({
+ queryKey: ['user', id],
+ queryFn: async () => userSchema.parse(await api.getUser(id)),
+ })
+}
+```
+
+## Lists
+
+- Use `FlatList`/`SectionList` (or `FlashList` for large/heavy lists) — never `.map()` a large array inside a `ScrollView`.
+- Provide a stable `keyExtractor`; memoize `renderItem`.
+- Paginate or virtualize long data sets.
+
+## Custom Hooks
+
+- Extract reusable logic (data, permissions, device APIs) into `use*` hooks.
+- Keep side effects (Expo SDK calls, subscriptions) inside hooks, not in JSX.
+
+## Async & Effects
+
+- Clean up subscriptions, timers, and listeners in the effect's return function.
+- Cancel or ignore stale async results on unmount to avoid setState-after-unmount.
diff --git a/rules/react-native/performance.md b/rules/react-native/performance.md
new file mode 100644
index 00000000..b96af5cd
--- /dev/null
+++ b/rules/react-native/performance.md
@@ -0,0 +1,45 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Performance
+
+> This file extends [common/performance.md](../common/performance.md) with React Native / Expo specific content.
+
+## Rendering
+
+- Memoize expensive components with `React.memo`; memoize callbacks/values passed to children with `useCallback`/`useMemo` only where they prevent real re-renders.
+- Keep component state local and narrow — lifting state too high re-renders large subtrees.
+- Avoid creating new objects/arrays/functions inline in props on hot paths; they break memoization.
+- Split large screens so a state change re-renders the smallest possible subtree.
+
+## Lists
+
+- Use `FlatList`/`SectionList`, or `FlashList` (Shopify) for large or heterogeneous lists.
+- Provide `keyExtractor`, a memoized `renderItem`, and stable item heights when possible (`getItemLayout`).
+- Tune `initialNumToRender`, `windowSize`, `maxToRenderPerBatch` for heavy rows.
+- Never render large data sets with `.map()` inside a `ScrollView`.
+
+## Images & Assets
+
+- Use `expo-image` for caching, priority, and placeholders; serve appropriately sized images.
+- Avoid loading full-resolution images into small thumbnails.
+
+## Animations
+
+- Prefer `react-native-reanimated` (runs on the UI thread) over the JS-driven `Animated` API.
+- For legacy `Animated`, set `useNativeDriver: true` where supported.
+- Keep heavy computation off the JS thread; offload to Reanimated worklets or native modules.
+
+## Runtime & Build
+
+- Build on the **New Architecture** (Fabric + TurboModules). It is the default in recent Expo SDKs (opt-out still available on SDK 53–54) and is mandatory — cannot be disabled — from SDK 55+. Verify every native dependency is New-Arch compatible before shipping.
+- Ensure **Hermes** is enabled (default in modern Expo) for faster startup and lower memory.
+- Defer non-critical work after first paint; lazy-load heavy screens/modules.
+- Use `InteractionManager.runAfterInteractions` for work that can wait until animations finish.
+
+## Measuring
+
+- Profile with the React DevTools profiler, the Hermes sampling profiler, and the in-app performance monitor. (Avoid Flipper — it is deprecated and not supported on the New Architecture.)
+- Watch for: long lists without virtualization, oversized images, frequent full-tree re-renders, and synchronous work on the JS thread.
diff --git a/rules/react-native/production-readiness.md b/rules/react-native/production-readiness.md
new file mode 100644
index 00000000..6ce720a9
--- /dev/null
+++ b/rules/react-native/production-readiness.md
@@ -0,0 +1,51 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Production Readiness
+
+> Extends the ECC philosophy to ship-grade concerns that style/pattern rules cannot encode by themselves.
+> A clean codebase is necessary but not sufficient for production — these items are mandatory before release.
+
+## Architecture
+
+- Ship on the **New Architecture** (Fabric + TurboModules). It is the default in recent Expo SDKs and is mandatory (cannot be disabled) from SDK 55+. Audit native deps for compatibility.
+- Pin the Expo SDK version; upgrade deliberately with `npx expo install --check` and test on both platforms.
+
+## Build & Release (EAS)
+
+- Use **EAS Build** for production binaries and **EAS Submit** for store delivery. Do not rely on local ad-hoc builds for release.
+- Keep separate build profiles (`development`, `preview`, `production`) in `eas.json`.
+- Manage signing credentials via EAS; never commit keystores or provisioning profiles.
+
+## Over-the-Air Updates
+
+- Use **EAS Update** (`expo-updates`) for JS-only fixes, with a defined runtime version policy.
+- Never push native changes via OTA — those require a new store build.
+- Roll out gradually and keep the ability to roll back.
+
+## Observability
+
+- Integrate crash + error reporting (e.g. **Sentry** via `@sentry/react-native`) in production builds.
+- Add structured logging and, where useful, analytics — but strip verbose logs from release.
+- Capture and surface failed network/mutation states; do not fail silently.
+
+## Configuration & Versioning
+
+- Bump `version` and `ios.buildNumber` / `android.versionCode` per release.
+- Public config via `EXPO_PUBLIC_*`; real secrets via EAS secrets only.
+- Validate required config at startup and fail fast with a clear message.
+
+## Pre-Release Gate
+
+Before shipping, all must pass:
+
+- [ ] `tsc --noEmit` clean
+- [ ] `npx expo lint` clean
+- [ ] Tests green, coverage >= 80% (see testing.md)
+- [ ] `npx expo-doctor` healthy
+- [ ] Critical-flow E2E (Maestro/Detox) pass on a real build
+- [ ] No secrets in bundle (see security.md)
+- [ ] Crash reporting active and verified
+- [ ] Tested on physical iOS and Android devices, not just simulators
diff --git a/rules/react-native/security.md b/rules/react-native/security.md
new file mode 100644
index 00000000..edd701ef
--- /dev/null
+++ b/rules/react-native/security.md
@@ -0,0 +1,43 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Security
+
+> This file extends [common/security.md](../common/security.md) with React Native / Expo specific content.
+> The mandatory pre-commit checklist and Security Response Protocol from common/security.md still apply.
+
+## The Bundle Is Public
+
+Treat everything shipped in the app as readable by an attacker. A mobile binary can be unpacked.
+
+- NEVER ship real secrets (private API keys, service-role keys, signing secrets) in the JS bundle or `app.config`.
+- Public/anon keys (e.g. Supabase anon key, Firebase config) are acceptable ONLY when protected by server-side rules (RLS, security rules). Enforce authorization on the backend, never in the client.
+- Keep privileged operations behind your own server / edge functions.
+
+## Secret & Token Storage
+
+- Store auth tokens and sensitive values in `expo-secure-store` (Keychain / Keystore) — never in `AsyncStorage` or plain MMKV.
+- Do not persist secrets in Redux/Zustand state that may be serialized to disk.
+
+## Configuration
+
+- Read environment via `expo-constants` / `app.config.ts` `extra`, and `EXPO_PUBLIC_*` only for genuinely public values.
+- Keep build secrets in EAS secrets, not in the repo.
+
+## Network & Data
+
+- HTTPS only; reject cleartext. Consider certificate pinning for high-risk apps.
+- Validate ALL external data (API responses, deep-link params, push payloads) with Zod before use.
+- Validate and sanitize deep links and universal links — never route or grant access based on unvalidated params.
+
+## Permissions & Privacy
+
+- Request the minimum device permissions, at the moment they are needed, with clear rationale.
+- Declare data collection accurately for App Store / Play Store privacy disclosures.
+
+## Dependencies
+
+- Run `expo-doctor` and `npm audit` regularly; keep the Expo SDK and native deps current.
+- Use `/security-scan` (AgentShield) on the agent configuration itself.
diff --git a/rules/react-native/testing.md b/rules/react-native/testing.md
new file mode 100644
index 00000000..628e3196
--- /dev/null
+++ b/rules/react-native/testing.md
@@ -0,0 +1,52 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.tsx"
+---
+# React Native / Expo Testing
+
+> This file extends [common/testing.md](../common/testing.md) with React Native / Expo specific content.
+> Coverage target and TDD workflow are inherited from common/testing.md (80% minimum, RED-GREEN-REFACTOR).
+
+## Tooling
+
+| Layer | Tool |
+|-------|------|
+| Unit / component | Jest + `@testing-library/react-native` (via `jest-expo` preset) |
+| Hooks | `@testing-library/react-native` `renderHook` |
+| E2E | Maestro (recommended, simple YAML flows) or Detox |
+| Type safety | `tsc --noEmit` in CI |
+
+## Component Tests
+
+- Query by accessible role/label/text, not by `testID` unless necessary — this also enforces accessibility.
+- Assert on user-visible behavior, not implementation details.
+- Follow Arrange-Act-Assert.
+
+```tsx
+import { render, screen, fireEvent } from '@testing-library/react-native'
+
+test('calls onSelect with the user id when pressed', () => {
+ const onSelect = jest.fn()
+ render()
+
+ fireEvent.press(screen.getByText('a@b.com'))
+
+ expect(onSelect).toHaveBeenCalledWith('1')
+})
+```
+
+## Mocking
+
+- Mock Expo SDK modules (camera, location, notifications, secure-store) at the test boundary.
+- Wrap components that use TanStack Query in a `QueryClientProvider` with a fresh client per test.
+- Mock navigation (`expo-router`) so screens render in isolation.
+
+## E2E
+
+- Cover critical flows only: auth, primary navigation, core transactions.
+- Run E2E on CI against a built app (EAS Build) before release.
+
+## What to test first
+
+Use the `tdd-guide` agent proactively for new features: write a failing test that captures the behavior, then implement.
diff --git a/skills/react-native-patterns/SKILL.md b/skills/react-native-patterns/SKILL.md
new file mode 100644
index 00000000..d0e6c327
--- /dev/null
+++ b/skills/react-native-patterns/SKILL.md
@@ -0,0 +1,326 @@
+---
+name: react-native-patterns
+description: React Native and Expo app patterns — Expo Router navigation, state separation (server/client/route/form), TanStack Query data fetching with Zod, performant lists, NativeWind/StyleSheet styling, native APIs, and secure storage. Use when building or editing React Native / Expo screens, components, navigation, or data layers.
+origin: ECC
+---
+
+# React Native / Expo Patterns
+
+Practical patterns for building production React Native apps with Expo. Covers navigation, state, data fetching, lists, styling, and native APIs. Pairs with the `rules/react-native/` ruleset: rules say *what* to enforce, this skill shows *how*.
+
+Libraries named below (NativeWind, Zustand/Jotai, TanStack Query) are common, well-established options shown for illustration — the patterns matter more than the specific package, and any equivalent works. Zod is used for validation to stay consistent with ECC's existing `typescript/` rules.
+
+These patterns assume the managed Expo workflow (Expo Router, EAS, `expo-*` modules) on the New Architecture (the default in recent Expo SDKs, mandatory from SDK 55+). They do NOT assume the browser DOM — React Native has no `
`, no URL bar, and no web data-fetching defaults.
+
+## When to Activate
+
+Use this skill when:
+
+- Building or editing React Native / Expo screens, components, or navigation
+- Setting up routing with Expo Router (file-based `app/` directory)
+- Deciding where state belongs (server cache vs client store vs route params vs form)
+- Wiring data fetching with TanStack Query and validating responses with Zod
+- Rendering long or heavy lists
+- Choosing or applying a styling approach (NativeWind or StyleSheet)
+- Accessing native device APIs (camera, location, notifications) or secure storage
+- Reviewing RN code for mobile-specific issues
+
+Do NOT use the web/React-DOM patterns here — URL-as-state, `
`, and SWR-for-browser do not apply to React Native.
+
+## Core Concepts
+
+### Project structure (Expo Router)
+
+File-based routing under `app/`. Keep route files thin: they read and validate params, then delegate to a screen component that lives in `components/` or `features/`.
+
+```
+app/
+ _layout.tsx # root stack
+ (tabs)/
+ _layout.tsx # tab navigator
+ index.tsx # Home
+ user/[id].tsx # dynamic route
+components/
+features/
+ user/UserProfile.tsx
+```
+
+### Navigation: validate route params
+
+Deep links and dynamic routes deliver untrusted strings. Validate them with Zod before use.
+
+```tsx
+// 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
+}
+```
+
+### State: keep concerns separate
+
+Do not duplicate server data into a client store. Each concern has its own home.
+
+| Concern | Common choices |
+|---------|------|
+| Server state (remote data) | a server-cache library (TanStack Query, SWR) |
+| Client/UI state | a lightweight store (Zustand, Jotai) or Context |
+| Route/navigation state | Expo Router params |
+| Form state | a form library (e.g. React Hook Form) + schema validation |
+| Secrets / tokens | `expo-secure-store` |
+| Non-secret persistence | `AsyncStorage` / MMKV |
+
+Prefer local `useState` until state genuinely needs sharing.
+
+### Data fetching: a cache library + Zod
+
+Use a server-cache library (TanStack Query, SWR) instead of fetch-in-`useEffect`. Validate at the boundary and infer types from the schema. Handle loading, error, and empty states explicitly. (Example uses TanStack Query.)
+
+```tsx
+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
+
+export function useUser(id: string) {
+ return useQuery({
+ queryKey: ['user', id],
+ queryFn: async (): Promise => 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] }),
+ })
+}
+```
+
+### Lists: virtualize, never map a big array in a ScrollView
+
+```tsx
+import { FlatList } from 'react-native'
+
+ item.id}
+ renderItem={renderItem} // memoized
+ initialNumToRender={10}
+ windowSize={5}
+/>
+```
+
+Use `FlashList` (Shopify) for large or heterogeneous lists.
+
+### Styling: pick one system
+
+`StyleSheet.create()` is the framework-native option; utility-class libraries (e.g. NativeWind) are a common alternative. Choose one and stay consistent. Never build style objects inline in JSX on hot paths.
+
+```tsx
+// NativeWind
+
+ Hello
+
+
+// StyleSheet
+const styles = StyleSheet.create({ card: { padding: 16, borderRadius: 16, backgroundColor: '#fff' } })
+...
+```
+
+### Native APIs: wrap in hooks, clean up effects
+
+Keep Expo SDK calls and subscriptions inside `use*` hooks, not in JSX. Always clean up.
+
+```tsx
+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() {
+ // Track status, not just coords — so the UI can tell "still loading" apart
+ // from "permission denied" and show an actionable message.
+ const [state, setState] = useState({ 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 } // ignore stale result after unmount
+ }, [])
+
+ return state
+}
+```
+
+### Secure storage for tokens
+
+```tsx
+import * as SecureStore from 'expo-secure-store'
+
+await SecureStore.setItemAsync('auth_token', token) // Keychain / Keystore
+const token = await SecureStore.getItemAsync('auth_token')
+```
+
+## Code Examples
+
+### A full screen: route → query → list → states
+
+```tsx
+// 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
+
+function useOrders() {
+ return useQuery({
+ queryKey: ['orders'],
+ queryFn: async () => OrdersSchema.parse(await api.listOrders()),
+ })
+}
+
+// Memoized so its reference is stable across renders (see the lists guidance).
+const OrderRow = memo(function OrderRow({ item }: { item: Order }) {
+ return (
+
+ #{item.id}
+ {item.status} · ${item.total}
+
+ )
+})
+
+export default function OrdersScreen() {
+ const { data, isLoading, isError, refetch, isRefetching } = useOrders()
+ const renderItem = useCallback(({ item }: { item: Order }) => , [])
+
+ if (isLoading) return Loading…
+ if (isError) return Could not load orders.
+ if (!data?.length) return No orders yet.
+
+ return (
+ o.id}
+ onRefresh={refetch}
+ refreshing={isRefetching}
+ renderItem={renderItem}
+ />
+ )
+}
+```
+
+### A form: React Hook Form + Zod resolver
+
+```tsx
+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('Invalid email') })
+type FormValues = z.infer
+
+export function EmailForm({ onSubmit }: { onSubmit: (v: FormValues) => void }) {
+ const { control, handleSubmit, formState: { errors } } = useForm({
+ resolver: zodResolver(Schema),
+ defaultValues: { email: '' },
+ })
+
+ return (
+ <>
+ (
+
+ )}
+ />
+ {errors.email && {errors.email.message}}
+
+ >
+ )
+}
+```
+
+## Anti-Patterns
+
+```tsx
+// WRONG: large array mapped inside a ScrollView (no virtualization, janky, high memory)
+{items.map((i) => )}
+// RIGHT: FlatList / FlashList
+
+// WRONG: server data copied into a client store (two sources of truth, stale data)
+const useStore = create((set) => ({ users: [], setUsers: (u) => set({ users: u }) }))
+useEffect(() => { getUsers().then(setUsers) }, [])
+// RIGHT: useQuery owns server state; derive what you need
+
+// WRONG: tokens in AsyncStorage (not encrypted)
+await AsyncStorage.setItem('auth_token', token)
+// RIGHT: expo-secure-store
+
+// WRONG: trusting deep-link params
+const { id } = useLocalSearchParams(); fetchUser(id)
+// RIGHT: validate with Zod before use
+
+// WRONG: inline style object recreated every render on a hot path
+
+// RIGHT: StyleSheet.create at module scope, or NativeWind className
+
+// WRONG: real secret shipped in the bundle
+const STRIPE_SECRET = 'sk_live_...'
+// RIGHT: keep privileged calls server-side; ship only public keys protected by backend rules
+```
+
+## Best Practices
+
+- Keep route files thin; put logic in screen components and `use*` hooks.
+- Validate every external input (API responses, route params, push payloads) with Zod.
+- Let TanStack Query own server state; keep client stores small.
+- Always render loading, error, and empty states — never just a spinner with no fallback.
+- Virtualize lists; memoize `renderItem`; provide a stable `keyExtractor`.
+- Use `react-native-reanimated` for animation (UI thread); avoid heavy work on the JS thread.
+- Store tokens in `expo-secure-store`; never trust the client for authorization.
+- Respect safe areas, Dynamic Type, and accessibility roles/labels from the start.
+- Confirm New Architecture compatibility for every native dependency before release.
+
+## Related Skills
+
+- `frontend-patterns` — React/Next.js (web) patterns; useful for shared React concepts, but DOM-specific.
+- `coding-standards` — TypeScript/JavaScript idioms that apply to RN code.
+- `tdd-workflow`, `e2e-testing` — testing process (use Jest + React Native Testing Library, Maestro/Detox for RN).
+- `security-review` — general security checklist that complements the RN bundle/secret guidance above.