---
paths:
  - "**/app.json"
  - "**/app.config.*"
  - "**/eas.json"
  - "**/metro.config.*"
  - "**/babel.config.*"
  - "**/app/**"
  - "**/*.tsx"
---
# React Native + Expo

How mobile apps are built — distilled from a hand-crafted reference app, the gold standard for *style*. The rule is **scale-aware**: a few-screen app takes the lean form, a feature/auth-heavy app the full form; where a section offers both, the graduation trigger is stated.

## 1. Stack & packages

- **Expo SDK** managed workflow, config-as-code (`app.config.ts`, **never** `app.json`), `expo-router` navigation.
- New Architecture **on**; off **only** for a legacy native module that breaks under it — document why at the top of `app.config.ts`.
- `react` / `react-dom` versions **must** match. Install native/Expo modules with `npx expo install <pkg>` — a bare add picks a version the SDK may not support.

| Need | Use | Don't use |
| --- | --- | --- |
| HTTP | native `fetch` wrapped in a `request<T>()` util | `axios` |
| Navigation | `expo-router` (file-based) | bare React Navigation at the root |
| Storage (sensitive) | `expo-secure-store` | `AsyncStorage` |
| Storage (prefs/cache) | `@react-native-async-storage/async-storage` | `expo-secure-store` |
| State (non-trivial) | `@reduxjs/toolkit` + `react-redux` | `zustand`, `jotai` |
| Styling | `StyleSheet.create` + token object | `nativewind`, `tamagui`, styled-components |
| Toasts | `react-native-toast-message` | custom |
| Haptics | `expo-haptics` behind one dispatcher | scattered call sites |

## 2. Structure & naming

- **Path alias `@/*` → `./src/*`** (`tsconfig.json`). Import via `@/...`, **never** deep-relative (`../../..`); sibling/child relatives are fine.
- Every file opens with a path + one-line purpose comment; one line above each function.
- **No barrel/index re-export files** — import the source module directly.
- Components & screens `PascalCase.tsx`; hooks `useXxx.ts`; services/utils/reducers/types `camelCase.ts`; route files follow `expo-router` (`index.tsx`, `[id].tsx`, `_layout.tsx`).

**Layout** — `app/` is routes only (§3); everything else under `src/`:

```
app/          # expo-router routes ONLY
src/
  components/ # presentational primitives + shared UI
  hooks/
  services/   # api client, auth storage, background tasks, notifications
  state/  or  reducers/ + store/   # §4 — scale decides
  types/      # shared TS contracts
  utils/      # pure helpers
  constants/  # Colors.ts, routes.ts, toast_config.tsx
  navigation/ # only if you outgrow Stack/Tabs
```

> **Lean form:** a handful of screens groups by domain instead — e.g. `src/{data,player,state,ui}`, `app/` still routes-only. Graduate to the by-type layout past >~3 cross-cutting concerns (auth + tabs + notifications + ...).

## 3. Expo Router & navigation

- A route file may hold its own data/business logic — the strict container/presentational split is not required, but pure UI primitives (`src/components/`) stay pure.
- `experiments.typedRoutes: true`. Navigate with `router.push()` / `router.replace()` (`replace` for auth/redirect transitions); `<Link>` **only** for static in-content links; params via `useLocalSearchParams()`.
- Route groups `(auth)` / `(tabs)` partition without affecting the URL; an `index.tsx` may be pure redirect logic, not a screen.
- **Root `_layout.tsx` is the provider tree**, outside-in, only what the app needs: `ErrorBoundary → Redux Provider → HydrationGate → GestureHandlerRootView → SafeAreaProvider → StatusBar → <Stack/Tabs> → Toast`. Lean form: `StatusBar → Stack` plus a one-time `useEffect` bootstrap.
- Header styling via `screenOptions` on the `Stack`; per-screen title via `<Stack.Screen options={{ title }}>`. `createBottomTabNavigator` inside `(tabs)/_layout.tsx` is acceptable over router `Tabs` when a custom header/drawer needs fine layout control.

## 4. State

- **Trivial** (no auth, few screens) — plain TS modules under `src/state/`: one concern per module, async get/set functions, `AsyncStorage` persistence with swallowed try/catch, optional module-level cache.
- **Non-trivial** (auth, preloaded config, cross-screen data) — **Redux Toolkit**: slices in `src/reducers/`, store in `src/store/`, one root reducer.
  - Persist with listener middleware (debounced ~300ms), **not** `redux-persist`.
  - Dual storage: tokens → `expo-secure-store`; UI prefs → `AsyncStorage`.
  - **Hydrate before first render** — async hydrate thunks behind a `HydrationGate` spinner, so no flash of unauthenticated UI.
  - Store absolute expiries (`accessExpiresAt`), computed at login from the `expiresIn` the server returns.

## 5. Data fetching / networking

- Native `fetch`, wrapped once: `request<T>(method, endpoint, body?, opts?)` in `src/services/api.ts`, plus a `useRequest<T>()` hook returning `{ loading, response, error, makeRequest }`.
- Base URL and auth token are read **at call time** (from Redux state or the settings module), never baked into a client instance; inject `Authorization: Bearer <token>` only when `authorized: true`.
- Per-call `onSuccess` / `onFail` callbacks, no global interceptor; failures surface as toasts or inline error UI.
- **Every fetched surface renders loading, empty, and error states** (error with a Retry affordance). Cache in-memory at the data module (lean) or in Redux slices (full).
- A `RequestMethod` union and a generic `ApiResponse<T>`; endpoint-specific wrappers live next to their screen (`app/<feature>/api.ts`).

## 6. Styling & theming

- `StyleSheet.create` for static styles, inline only for dynamic theme values. **No** utility-CSS or styled-component libs (§1 table).
- Color tokens live in one place (`src/constants/Colors.ts`) — components reference tokens, **never** raw hex.
- **Theming is a hook, not a Context provider.** `useTheme()` returns `{ colors, colorScheme, isDark }`, resolving user preference → device `useColorScheme()` → light fallback. The preference lives in state (`customize.theme`: `"light" | "dark" | "system"`), so theme survives restart and there is no provider to thread.

  > **Lean form:** a flat single-mode token object until dark mode is a requirement; then adopt the light/dark `Colors` + `useTheme()` shape.

## 7. Native modules & permissions

- **Wrap each `expo-*` module behind a typed helper** — never raw calls at call sites. Canonical: one `triggerHaptic(type)` dispatcher with named exports (`triggerSuccessHaptic`, `triggerTabChangeHaptic`, ...) over `expo-haptics`.
- Platform forks via filename suffixes (`Foo.native.tsx` / `Foo.web.tsx` / `Foo.tsx`); `Platform.OS` checks **only** for small inline branches.
- Android/iOS permissions and usage strings are declared in `app.config.ts`; request at point-of-use with a clear pre-prompt where the platform allows.
- Background work (location, audio) runs through a registered task (`expo-task-manager` / a playback service), registered in the app entry **before the router mounts** — it must survive the UI being unmounted.

## 8. Reusable primitives

- `Themed{View,Text,Button}` are the base layer: extend the native props, add `lightColor?` / `darkColor?`; `ThemedText` takes `type?: "default" | "title" | "subtitle" | "link"`; `ThemedButton` takes `variant?: "contained" | "outlined" | "text"`, `size?`, `fullWidth?`.
- `EdgeToEdgeScreen` / `EdgeToEdgeTabScreen` / `EdgeToEdgeTabBar` own safe-area insets — screens **never** compute insets inline or ad hoc.
- Loading/feedback primitives (`DotLoader`, `CircularProgress`, `PullToRefreshHint`) are shared, never re-implemented per screen.

## 9. TypeScript

- `interface` for external contracts (API shapes, component props); `type` for unions and aliases (`type RequestMethod = "get" | ...`).
- Shared domain types in `src/types/`; prop types extend native ones (`TextProps & { ... }`).
- **Never weaken `strict` or `noUncheckedIndexedAccess`** — extend `expo/tsconfig.base`, don't relax it. Generic `request<T>` / `useRequest<T>`; no implicit `any` on external data.

## 10. Config & environment

- `app.config.ts` is the single source: scheme, deep-link intent filters, permissions, plugins, `userInterfaceStyle: "automatic"`, typed routes (§3), splash/icon.
- Client-readable env via `EXPO_PUBLIC_*` **only**, read through `expo-constants` or `process.env`. **Never commit `.env`** — ship `.env.example`; secrets/keystore creds stay out of git.
- Build profiles in `eas.json` (`development` / `preview` / `production`); local config plugins (`./plugins/withXxx.js`) for native edits the SDK doesn't expose.

## 11. Errors, loading & UX

- **An `ErrorBoundary` class component wraps the whole app.** On crash: log, clear *session* state but **preserve user preferences**, show an apology + recover affordance.
- Toasts via `react-native-toast-message` with a custom `toast_config.tsx` (success/error/warning variants, `topOffset` below the header); fired inline with `Toast.show({ type, text1, text2 })`.
- Hide the splash only after fonts load and state hydrates (`expo-splash-screen`).
- Lists: `FlatList` typed with `ListRenderItemInfo<T>`, `ListEmptyComponent` for empty, `onEndReached` + pagination for infinite scroll, `refreshing`/`onRefresh` for pull-to-refresh.

## 12. Web target (`expo start --web`)

- Web needs `react-dom`, `react-native-web`, `@expo/metro-runtime`, and a `"web": "expo start --web"` script.
- A blank page with a bundle resolve failure means `react-native-track-player` is missing its web peer `shaka-player`.
- Native-only features (background audio, lock-screen controls, Android Auto) have no web equivalent — guard them behind `Platform.OS` so the web build renders, and treat web as a layout/navigation preview, **never** a playback/Auto test surface.

## 13. Formatting

- Prettier: `tabWidth: 4`, double quotes, `bracketSameLine: true`, `printWidth: 120`, trailing commas.
- No commented-out code; comments explain *why*, not *what*.
