---
name: react-native-architecture
description: Architect-level guidance for React Native apps — Expo vs bare workflow, New Architecture (Fabric/TurboModules), navigation (Expo Router / React Navigation), state and offline-first data, native module strategy, monorepo sharing with web, theming, and release engineering with EAS. Use this whenever the user is starting or restructuring a React Native / Expo project, choosing libraries for navigation, storage, or networking, deciding whether to write a native module, asking about code sharing between React Native and React web, or asking how to organise a mobile codebase. Also apply it when reviewing React Native code for architectural issues.
metadata:
  technology: React Native
  type: architecture
---

# React Native Architecture

Mobile is a different discipline from web: binaries ship through app stores, users are offline, devices are slow, and mistakes take a week to roll back. Design for that.

## 1. Platform decisions (write them into ARCHITECTURE.md)

| Decision | Default | Change when |
|---|---|---|
| Workflow | **Expo (managed, with `expo-dev-client` and config plugins)** | You must ship a native SDK with no config plugin and can't write one → bare, but keep `expo` modules. "Ejecting" is rarely justified now. |
| Architecture | **New Architecture enabled** (Fabric + TurboModules, bridgeless) | Only a legacy dependency blocks it — then plan the migration, don't disable forever. |
| JS engine | **Hermes** | Never Jsc. |
| Navigation | **Expo Router** (file-based, deep links for free) | Existing large React Navigation codebase → stay on React Navigation v7. |
| Styling | **StyleSheet + a token file**, or **NativeWind** if the team is Tailwind-fluent | Design system requires runtime theming across many brands → Tamagui / Unistyles. |
| Language | TypeScript strict | Never. |
| Package manager | pnpm with `node-linker=hoisted` (RN tooling needs hoisting) | Yarn in existing monorepo. |
| Builds/updates | **EAS Build + EAS Update** | Enterprise CI requirement → Fastlane + self-hosted, still with Expo modules. |

## 2. Folder structure

Same feature-first principle as React web, with mobile-specific additions:

```
src/
├── app/                # Expo Router routes only — thin files that render feature screens
│   ├── (auth)/
│   ├── (tabs)/
│   └── _layout.tsx
├── features/<feature>/ # screens/, components/, hooks/, api/, model/, index.ts
├── shared/
│   ├── ui/             # primitives; every one supports light/dark and dynamic type
│   ├── lib/            # api client, storage, analytics, logger, permissions
│   ├── hooks/
│   └── theme/          # tokens: colors, spacing, typography, radii
├── native/             # local Expo modules / TurboModules (Swift/Kotlin) with TS bindings
└── assets/
```

Rules:
- Route files in `app/` contain no business logic; they import a screen from a feature. Keeps routes testable and the router swappable.
- Platform forks via `Component.ios.tsx` / `Component.android.tsx` only for real divergence; prefer `Platform.select` inside one file for small differences.
- `shared/ui` never imports from `features/`.

## 3. Navigation

- Group routes by auth state: `(auth)` and `(app)` groups with a root `_layout.tsx` that redirects based on session.
- Typed routes on (`experiments.typedRoutes`) so `router.push` params are checked.
- Deep links and universal links configured from day one (`scheme`, `associatedDomains`, `intentFilters`). Retrofitting is painful.
- Modals as route groups (`(modals)`), not component state — they need to be deep-linkable and back-button aware.
- Keep tab bars to ≤ 5; nest stacks inside tabs, not tabs inside stacks.

## 4. State and data — offline first

Mobile networks fail constantly. The architecture must assume it:

| Kind | Tool |
|---|---|
| Server data | **TanStack Query** with `persistQueryClient` to MMKV and `networkMode: 'offlineFirst'` |
| Mutations while offline | Query mutation queue + `onlineManager`; or a dedicated outbox table for critical writes |
| Key-value (tokens, settings) | **MMKV** (`react-native-mmkv`) — synchronous, fast. Never AsyncStorage for hot paths. |
| Secrets | `expo-secure-store` (Keychain / Keystore) — tokens go here, not MMKV |
| Relational / large local data | **Expo SQLite** (with Drizzle) or **WatermelonDB** for sync-heavy apps |
| Client state | Zustand with MMKV persistence |
| Forms | React Hook Form + Zod |

Design the sync story explicitly: last-write-wins vs server-authoritative vs CRDT. Document it. Show the user the conflict case before they pick.

## 5. Native modules — when and how

Decide in this order:
1. **Does an Expo SDK module exist?** Use it (`expo-camera`, `expo-location`, `expo-notifications`…). They have config plugins and New Architecture support.
2. **Does a well-maintained community module exist with a config plugin?** Check New Architecture support, last release date, open issues.
3. **Write a local Expo Module** (`npx create-expo-module --local`) in Swift/Kotlin. Small, typed, no bridge boilerplate. This is the default for custom native code.
4. **TurboModule / Fabric component by hand** only for performance-critical rendering or when integrating an existing large native codebase.

Never patch `node_modules` without `patch-package` and a tracking issue. Never run `expo prebuild` output into git unless you have fully committed to the bare workflow.

## 6. Sharing code with web

- Monorepo (Turborepo/Nx): `apps/mobile`, `apps/web`, `packages/core` (domain logic, API client, Zod schemas, query hooks), `packages/ui` (platform-agnostic primitives only if genuinely shared).
- Share **logic**, not screens. Attempts to share full UI via `react-native-web` succeed for simple apps and fight you for complex ones; be honest about that.
- `packages/core` has no `react-native` imports. Enforce with ESLint.

## 7. Theming, accessibility, i18n — built in, not bolted on

- Tokens in `shared/theme`; components read via a `useTheme()` hook; support `useColorScheme()` from day one.
- Respect Dynamic Type: use `allowFontScaling` defaults, test at 200%.
- Every touchable has `accessibilityRole`, `accessibilityLabel`; hit targets ≥ 44×44.
- i18n via `i18next` + `expo-localization`; no hard-coded strings in screens; RTL tested with `I18nManager.forceRTL` in dev.

## 8. Observability and quality gates

- Crash reporting (Sentry) with source maps uploaded per build; wrap the root in an error boundary that shows a designed screen and offers "reload".
- Performance monitoring on cold start, screen TTI, and JS thread FPS (see `react-native-performance` skill).
- Feature flags from a remote config so risky features can be killed without a store release.
- `expo-doctor`, `tsc`, ESLint (`eslint-plugin-react-native`, `eslint-plugin-react-hooks`), and Jest run in CI on every PR; EAS Build preview per PR.

## 9. Release engineering

- Semantic versioning for `version`; auto-increment `buildNumber`/`versionCode` in EAS.
- Channels: `development` (dev client), `preview` (internal testers), `production`. EAS Update for JS-only fixes; native changes require a store build — teach the team which is which.
- Runtime version policy `appVersion` so updates never load against an incompatible native binary.
- Staged rollout (Play Console %) and phased release (App Store) always on for production.
- Keep a `RELEASE.md` runbook: who bumps, how to roll back an update (`eas update --republish`), how to hotfix.

## Anti-patterns to flag

- AsyncStorage for tokens or hot reads.
- Business logic in route files or in `_layout.tsx`.
- `Dimensions.get('window')` at module scope (breaks rotation/foldables); use `useWindowDimensions`.
- Inline `require('./img.png')` scattered everywhere — centralise assets.
- Disabling the New Architecture "for now" without a dated migration ticket.
- Fetching without offline handling and calling it done.
- Copying web components into RN and wrapping them in `View` without redesigning for touch.
