A production-ready starter I use as the foundation for React Native apps. TypeScript-strict, opinionated, and small enough to read in one sitting.
Built from the patterns I use in production apps (enterprise employee & partner apps shipped to the App Store and Google Play) — rewritten from scratch as a clean, reusable template.
| Concern | Choice | Why |
|---|---|---|
| Language | TypeScript (strict) | Bugs at compile time, typed navigation params |
| Framework | Expo | OTA updates, config plugins, fastest DX |
| Navigation | React Navigation (native stack) | Native transitions, typed route map |
| Server state | React Query | Caching, retries, loading/error states for free |
| Client state | Zustand | Tiny, no boilerplate, works outside React (interceptors) |
| Storage | MMKV | Synchronous & ~30x faster than AsyncStorage |
| Networking | Axios + interceptors | Auth header injection, normalized errors, 401 auto-logout |
| i18n | i18next | Device-locale detection, JSON per language |
| Theming | Design tokens + useTheme |
Light/dark from one palette, no hardcoded hex |
Server data lives in React Query. Client state lives in Zustand.
Most RN state-management pain comes from mixing these. Keeping them separate means no manual caches, no stale flags, no "loading" booleans in stores.
src/
├── App.tsx # provider composition (SafeArea → Query → Navigation)
├── api/ # axios client + interceptors (auth, error normalization)
├── components/ # shared UI (Screen wrapper, AppButton)
├── config/ # env access — fails fast on missing vars
├── i18n/ # i18next setup + locales/en.json, hi.json
├── navigation/ # RootNavigator + typed RootStackParamList
├── query/ # React Query client + per-resource hooks (useUsers)
├── screens/ # feature screens, consume hooks only
├── state/ # Zustand stores (session, persisted via MMKV)
├── storage/ # MMKV instance + Zustand adapter
└── theme/ # design tokens, light/dark, useTheme()
Screen → useUsers() → React Query → api client → interceptor adds token
↑ from Zustand store
← loading / error / data states rendered declaratively
A 401 anywhere clears the session store; navigation reacts automatically.
Screens never import axios and never parse errors — they get a normalized
{ status, message }.
npx degit rai-ms/rn-base my-app && cd my-app
cp .env.example .env # set API_BASE_URL
npm install
npm run start- New API resource → copy
src/query/useUsers.ts, change the type + key + path - New screen → add to
RootStackParamList, register inRootNavigator - New language → drop
locales/xx.json, register insrc/i18n/index.ts - Auth flow → call
useSessionStore.getState().setSession(token)after login
MIT — use it for anything.