React Native Architecture
Production patterns for React Native with Expo, covering project setup, navigation, state management, native integration, offline-first, performance, and CI/CD.
When to Use
-
Starting a new React Native or Expo project
-
Implementing complex navigation patterns
-
Integrating native modules and platform APIs
-
Building offline-first mobile applications
-
Optimizing React Native performance
-
Setting up CI/CD for mobile releases
Project Setup (Expo)
Create new Expo project
npx create-expo-app@latest my-app --template tabs cd my-app
Or with blank TypeScript template
npx create-expo-app@latest my-app -t expo-template-blank-typescript
Recommended Project Structure
src/ app/ # Expo Router file-based routes (tabs)/ # Tab navigator group index.tsx # Home tab profile.tsx # Profile tab _layout.tsx # Root layout +not-found.tsx # 404 screen components/ # Shared UI components ui/ # Primitives (Button, Input, Card) hooks/ # Custom hooks lib/ # Utilities, API client, storage store/ # State management (Zustand) constants/ # Theme, config, enums types/ # TypeScript definitions
Navigation
Expo Router (Recommended)
File-based routing, similar to Next.js.
// app/_layout.tsx — Root layout import { Stack } from 'expo-router';
export default function RootLayout() { return ( <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="modal" options={{ presentation: 'modal' }} /> </Stack> ); }
// app/(tabs)/_layout.tsx — Tab navigator import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />, }} /> <Tabs.Screen name="profile" options={{ title: 'Profile' }} /> </Tabs> ); }
Navigation Patterns
Pattern Implementation
Stack <Stack> in layout — push/pop screens
Tabs <Tabs> in layout — bottom tab bar
Drawer expo-router with drawer layout
Modal presentation: 'modal' in screen options
Deep linking Automatic with Expo Router file paths
Auth flow Conditional layout based on auth state
Auth-Protected Routes
// app/_layout.tsx import { Redirect } from 'expo-router'; import { useAuth } from '@/hooks/useAuth';
export default function RootLayout() { const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <SplashScreen />; if (!isAuthenticated) return <Redirect href="/login" />;
return <Stack />; }
State Management
Scope Solution
Component useState , useReducer
Server data TanStack Query (React Query)
Global UI Zustand
Persistent MMKV + Zustand persist
Forms React Hook Form
Zustand Store
import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { zustandStorage } from '@/lib/mmkv-storage';
interface AppStore { theme: 'light' | 'dark'; setTheme: (theme: 'light' | 'dark') => void; }
export const useAppStore = create<AppStore>()( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'app-store', storage: createJSONStorage(() => zustandStorage) } ) );
TanStack Query for API Data
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUser(id: string) { return useQuery({ queryKey: ['user', id], queryFn: () => api.getUser(id), staleTime: 5 * 60 * 1000, }); }
function useUpdateUser() { const qc = useQueryClient(); return useMutation({ mutationFn: api.updateUser, onSuccess: (_, vars) => qc.invalidateQueries({ queryKey: ['user', vars.id] }), }); }
Native Modules & Platform APIs
Using Expo Modules
npx expo install expo-camera expo-location expo-notifications expo-file-system
import * as Location from 'expo-location';
async function getLocation() { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') throw new Error('Permission denied'); return Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High }); }
Platform-Specific Code
import { Platform } from 'react-native';
const styles = { shadow: Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }, android: { elevation: 4 }, }), };
// File-based: MyComponent.ios.tsx / MyComponent.android.tsx
Offline-First Architecture
Storage Options
Solution Use For Speed
MMKV Key-value, small data, preferences Fastest
SQLite (expo-sqlite) Structured data, queries, relations Fast
AsyncStorage Legacy key-value (avoid for new) Slow
FileSystem Large files, downloads, cache Varies
Offline Queue Pattern
import NetInfo from '@react-native-community/netinfo';
class OfflineQueue { private queue: PendingAction[] = [];
async enqueue(action: PendingAction) { this.queue.push(action); await this.persist(); this.processIfOnline(); }
private async processIfOnline() { const { isConnected } = await NetInfo.fetch(); if (!isConnected) return;
while (this.queue.length > 0) {
const action = this.queue[0];
try {
await this.execute(action);
this.queue.shift();
await this.persist();
} catch {
break; // Retry later
}
}
} }
Performance
Optimization Checklist
Area Action
Lists Use FlashList instead of FlatList
Images Use expo-image (caching, blurhash, transitions)
Animations Use react-native-reanimated (UI thread)
Heavy computation Move to worklet or background thread
Re-renders Profile with React DevTools, apply memo
Bundle Use Hermes engine (default in Expo 49+)
Startup Minimize root component, lazy load screens
FlashList (Drop-in FlatList Replacement)
import { FlashList } from '@shopify/flash-list';
<FlashList data={items} renderItem={({ item }) => <ItemCard item={item} />} estimatedItemSize={80} keyExtractor={(item) => item.id} />
Image Optimization
import { Image } from 'expo-image';
<Image source={{ uri: imageUrl }} placeholder={{ blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH' }} contentFit="cover" transition={200} style={{ width: 200, height: 200 }} />
CI/CD with EAS Build
Setup
npm install -g eas-cli eas login eas build:configure
eas.json
{ "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal", "ios": { "simulator": true } }, "production": {} }, "submit": { "production": { "ios": { "appleId": "you@example.com", "ascAppId": "123456789" }, "android": { "serviceAccountKeyPath": "./google-services.json" } } } }
Build Commands
eas build --platform ios --profile preview # iOS simulator build eas build --platform android --profile preview # Android APK eas build --platform all --profile production # Production builds eas submit --platform all # Submit to stores eas update --branch preview --message "Bug fix" # OTA update
Project Checklist
-
Expo SDK latest (or target version)
-
TypeScript configured with strict mode
-
Expo Router for navigation
-
Zustand + MMKV for state persistence
-
TanStack Query for server state
-
FlashList for performant lists
-
expo-image for optimized images
-
Reanimated for animations
-
EAS Build configured for CI/CD
-
Deep linking tested
-
Offline handling implemented
-
Platform-specific code isolated
-
Error boundaries at route level