| name | react-native-architecture |
| description | Build production React Native apps with Expo, navigation, native modules, offline sync, and cross-platform patterns. Use when developing mobile apps, implementing native integrations, or architecting React Native projects. |
React Native Architecture
Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture.
When to Use This Skill
- 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
Core Concepts
1. Project Structure
src/
├── app/ # Expo Router screens
│ ├── (auth)/ # Auth group
│ ├── (tabs)/ # Tab navigation
│ └── _layout.tsx # Root layout
├── components/
│ ├── ui/ # Reusable UI components
│ └── features/ # Feature-specific components
├── hooks/ # Custom hooks
├── services/ # API and native services
├── stores/ # State management
├── utils/ # Utilities
└── types/ # TypeScript types
2. Expo vs Bare React Native
| Feature | Expo | Bare RN |
|---|
| Setup complexity | Low | High |
| Native modules | EAS Build | Manual linking |
| OTA updates | Built-in | Manual setup |
| Build service | EAS | Custom CI |
| Custom native code | Config plugins | Direct access |
Quick Start
npx create-expo-app@latest my-app -t expo-template-blank-typescript
npx expo install expo-router expo-status-bar react-native-safe-area-context
npx expo install @react-native-async-storage/async-storage
npx expo install expo-secure-store expo-haptics
import { Stack } from 'expo-router'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { QueryProvider } from '@/providers/QueryProvider'
export default function RootLayout() {
return (
<QueryProvider>
<ThemeProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="(auth)" />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
</QueryProvider>
)
}
Patterns
Pattern 1: Expo Router Navigation
import { Tabs } from 'expo-router'
import { Home, Search, User, Settings } from 'lucide-react-native'
import { useTheme } from '@/hooks/useTheme'
export default function TabLayout() {
const { colors } = useTheme()
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.textMuted,
tabBarStyle: { backgroundColor: colors.background },
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
}}
/>
,
}}
/>
,
}}
/>
,
}}
/>
)
}
{ useLocalSearchParams }
() {
{ id } = useLocalSearchParams<{ : }>()
}
{ router }
router.()
router.()
router.()
router.({
: ,
: { : , : },
})
Pattern 2: Authentication Flow
import { createContext, useContext, useEffect, useState } from 'react'
import { useRouter, useSegments } from 'expo-router'
import * as SecureStore from 'expo-secure-store'
interface AuthContextType {
user: User | null
isLoading: boolean
signIn: (credentials: Credentials) => Promise<void>
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
const segments = useSegments()
const router = useRouter()
( {
()
}, [])
( {
(isLoading)
inAuthGroup = segments[] ===
(!user && !inAuthGroup) {
router.()
} (user && inAuthGroup) {
router.()
}
}, [user, segments, isLoading])
() {
{
token = .()
(token) {
userData = api.(token)
(userData)
}
} (error) {
.()
} {
()
}
}
() {
{ token, user } = api.(credentials)
.(, token)
(user)
}
() {
.()
()
}
(isLoading) {
}
(
)
}
= () => {
context = ()
(!context) ()
context
}
Pattern 3: Offline-First with React Query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import AsyncStorage from '@react-native-async-storage/async-storage'
import NetInfo from '@react-native-community/netinfo'
import { onlineManager } from '@tanstack/react-query'
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected)
})
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24,
staleTime: 1000 * 60 * ,
: ,
: ,
},
: {
: ,
},
},
})
asyncStoragePersister = ({
: ,
: ,
})
() {
(
)
}
{ useQuery, useMutation, useQueryClient }
() {
({
: [],
: api.,
: previousData,
})
}
() {
queryClient = ()
({
: api.,
: (newProduct) => {
queryClient.({ : [] })
previous = queryClient.([])
queryClient.([], [
...old,
{ ...newProduct, : + .() },
])
{ previous }
},
: {
queryClient.([], context?.)
},
: {
queryClient.({ : [] })
},
})
}
Pattern 4: Native Module Integration
import * as Haptics from "expo-haptics";
import { Platform } from "react-native";
export const haptics = {
light: () => {
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
},
medium: () => {
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
},
heavy: () => {
if (Platform.OS !== "web") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}
},
success: () => {
if (Platform.OS !== "web") {
Haptics.(..);
}
},
: {
(. !== ) {
.(..);
}
},
};
* ;
(): <> {
hasHardware = .();
(!hasHardware) ;
isEnrolled = .();
(!isEnrolled) ;
result = .({
: ,
: ,
: ,
});
result.;
}
* ;
{ } ;
;
.({
: () => ({
: ,
: ,
: ,
}),
});
() {
: | ;
(. === ) {
.(, {
: ,
: ..,
: [, , , ],
});
}
{ : existingStatus } = .();
finalStatus = existingStatus;
(existingStatus !== ) {
{ status } = .();
finalStatus = status;
}
(finalStatus !== ) {
;
}
projectId = .?.?.?.;
token = ( .({ projectId })).;
token;
}
Pattern 5: Platform-Specific Code
import { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native'
import * as Haptics from 'expo-haptics'
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
interface ButtonProps {
title: string
onPress: () => void
variant?: 'primary' | 'secondary' | 'outline'
disabled?: boolean
}
export function Button({
title,
onPress,
variant = 'primary',
disabled = false,
}: ButtonProps) {
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale. }],
}))
= () => {
scale. = ()
(. !== ) {
.(..)
}
}
= () => {
scale. = ()
}
(
)
}
styles = .({
: {
: ,
: ,
: ,
: ,
....({
: {
: ,
: { : , : },
: ,
: ,
},
: {
: ,
},
}),
},
: {
: ,
},
: {
: ,
},
: {
: ,
: ,
: ,
},
: {
: ,
},
: {
: ,
: ,
},
: {
: ,
},
: {
: ,
},
: {
: ,
},
})
Pattern 6: Performance Optimization
import { FlashList } from '@shopify/flash-list'
import { memo, useCallback } from 'react'
interface ProductListProps {
products: Product[]
onProductPress: (id: string) => void
}
const ProductItem = memo(function ProductItem({
item,
onPress,
}: {
item: Product
onPress: (id: string) => void
}) {
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress])
return (
<Pressable onPress={handlePress} style={styles.item}>
<FastImage
source={{ uri: item.image }}
style={styles.image}
resizeMode="cover"
/>
<Text style=>{item.name}
${item.price}
)
})
() {
renderItem = (
(
),
[onProductPress]
)
keyExtractor = ( item., [])
(
)
}
EAS Build & Submit
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {
"autoIncrement": true
eas build --platform ios --profile development
eas build --platform android --profile preview
eas build --platform all --profile production
eas submit --platform ios
eas submit --platform android
eas update --branch production --message "Bug fixes"
Best Practices
Do's
- Use Expo - Faster development, OTA updates, managed native code
- FlashList over FlatList - Better performance for long lists
- Memoize components - Prevent unnecessary re-renders
- Use Reanimated - 60fps animations on native thread
- Test on real devices - Simulators miss real-world issues
Don'ts
- Don't inline styles - Use StyleSheet.create for performance
- Don't fetch in render - Use useEffect or React Query
- Don't ignore platform differences - Test on both iOS and Android
- Don't store secrets in code - Use environment variables
- Don't skip error boundaries - Mobile crashes are unforgiving
Resources