React Native Expo
Use this stack when Android and iOS both matter, React is already the fastest team skill, and the app can live inside the React Native ecosystem.
Good fits:
- Cross-platform product apps that need native distribution without two fully separate native codebases.
- Content, workflow, marketplace, media, fitness, education, and internal apps with normal device APIs.
- Solo-dev apps where Expo Router, config plugins, EAS Build, EAS Submit, and EAS Update remove native release toil.
- Apps that need a realistic path from prototype to app-store release without abandoning React.
Weak fits:
- Apps whose core value depends on deep platform APIs with no maintained React Native or Expo module.
- Permission-heavy apps where store policy evidence is uncertain.
- Graphics, games, low-latency media, or always-on background apps where native control matters more than shared UI.
- Teams that cannot test real Android and iOS builds before release.
Stack Shape
| Layer | Default | Why |
|---|---|---|
| App framework | Expo | React Native framework, SDK packages, CLI, app config, prebuild, and development loop |
| UI runtime | React Native | Native Android/iOS views driven by React |
| Routing | Expo Router | File-based routes, layouts, route groups, dynamic routes, typed routes, deep links, and web support |
| Development binary | Development build with expo-dev-client | Project-specific native shell that supports native dependencies and app config |
| Native generation | CNG with npx expo prebuild | Regenerates android/ and ios/ from config, plugins, dependencies, and SDK templates |
| Native customization | Config plugins and Expo Modules API | Durable native changes that survive prebuild and rebuilds |
| Build and submit | EAS Build and EAS Submit | Signed Android/iOS binaries and store uploads without hand-maintaining local signing flows |
| OTA updates | EAS Update | JavaScript, style, and asset updates inside a compatible runtime version |
| Quality | Jest, React Native Testing Library, router tests, built-app smoke, store-track checks | Mobile failures hide in native builds, devices, permissions, and store release paths |
Minimum Done Criteria
Do not call an Expo feature done until these are true:
| Area | Proof |
|---|---|
| Dependencies | npx expo install --check or equivalent compatibility review has no unexplained drift |
| JavaScript tests | Jest and React Native Testing Library tests pass for changed logic and screens |
| Router | Route files, layouts, params, and typed links are exercised by tests or a manual route smoke |
| Native boundary | Any native dependency, permission, plugin, SDK, app config, or module change has a rebuild plan |
| Development build | A development or preview build is installed when the change touches native behavior |
| Android release | A production AAB can be built or the blocker is documented |
| iOS release | A production IPA or TestFlight path is confirmed on a Mac/EAS path or the blocker is documented |
| OTA safety | Runtime version, branch, channel, rollout, and rollback path are known before eas update |
| Permissions | Android manifest, iOS usage strings, denied flows, and store explanations are checked |
| Secrets | No signing keys, service account keys, API secrets, provisioning assets, or EAS tokens are committed |
First Pages To Read
- Decision Guide
- Starter Architecture
- Commands
- Quality Gates
- Agent Guardrails
- Stack Overview
- Native Boundary
- Release Architecture
- Expo Router
- Development Builds
- OTA Update Model
- Testing Expo Apps
- Common Errors
Recommended Default
Start with one Expo app, Expo Router, TypeScript, a small src/ layer for API/hooks/state/components, app.config.ts or app.json, and EAS profiles for development, preview, and production.
Use Expo Go only for early learning and JavaScript-only prototypes. Move to a development build as soon as you add native libraries, custom permissions, app config, release signing, or anything store-bound.
Gotcha: EAS Update is not a release escape hatch. Native code, native dependencies, permissions, Expo SDK upgrades, app identifiers, and runtime-incompatible changes require a new native binary.