React Native Architecture
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.
MCP get_skill({ skillId: "react-native-architecture-d8085005" })Use this skill with your agent
Create a free account and connect via MCP
# 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
```bash
# Create new Expo project
npx create-expo-app@latest my-app -t expo-template-blank-typescript
# Install essential dependencies
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
```
```typescript
// app/_layout.tsx
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
```typescript
// app/(tabs)/_layout.tsx
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} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'Search',
tabBarIcon: ({ color, size }) => <Search size={size} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
}}
/>
</Tabs>
)
}
// app/(tabs)/profile/[id].tsx - Dynamic route
import { useLocalSearchParams } from 'expo-router'
export default function ProfileScreen() {
const { id } = useLocalSearchParams<{ id: string }>()
return <UserProfile userId={id} />
}
// Navigation from anywhere
import { router } from 'expo-router'
// Programmatic navigation
router.push('/profile/123')
router.replace('/login')
router.back()
// With params
router.push({
pathname: '/product/[id]',
params: { id: '123', referrer: 'home' },
})
```
### Pattern 2: Authentication Flow
```typescript
// providers/AuthProvider.tsx
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()
// Check authentication on mount
useEffect(() => {
checkAuth()
}, [])
// Protect routes
useEffect(() => {
if (isLoading) return
const inAuthGroup = segments[0] === '(auth)'
if (!user && !inAuthGroup) {
router.replace('/login')
} else if (user && inAuthGroup) {
router.replace('/(tabs)')
}
}, [user, segments, isLoading])
async function checkAuth() {
try {
const token = await SecureStore.getItemAsync('authToken')
if (token) {
const userData = await api.getUser(token)
setUser(userData)
}
} catch (error) {
await SecureStore.deleteItemAsync('authToken')
} finally {
setIsLoading(false)
}
}
async function signIn(credentials: Credentials) {
const { token, user } = await api.login(credentials)
await SecureStore.setItemAsync('authToken', token)
setUser(user)
}
async function signOut() {
await SecureStore.deleteItemAsync('authToken')
setUser(null)
}
if (isLoading) {
return <SplashScreen />
}
return (
<AuthContext.Provider value={{ user, isLoading, signIn, signOut }}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
```
### Pattern 3: Offline-First with React Query
```typescript
// providers/QueryProvider.tsx
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'
// Sync online status
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected)
})
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 2,
networkMode: 'offlineFirst',
},
mutations: {
networkMode: 'offlineFirst',
},
},
})
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_OFFLINE_CACHE',
})
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
>
{children}
</PersistQueryClientProvider>
)
}
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
export function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: api.getProducts,
// Use stale data while revalidating
placeholderData: (previousData) => previousData,
})
}
export function useCreateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: api.createProduct,
// Optimistic update
onMutate: async (newProduct) => {
await queryClient.cancelQueries({ queryKey: ['products'] })
const previous = queryClient.getQueryData(['products'])
queryClient.setQueryData(['products'], (old: Product[]) => [
...old,
{ ...newProduct, id: 'temp-' + Date.now() },
])
return { previous }
},
onError: (err, newProduct, context) => {
queryClient.setQueryData(['products'], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
}
```
### Pattern 4: Native Module Integration
```typescript
// services/haptics.ts
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.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
},
error: () => {
if (Platform.OS !== "web") {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
},
};
// services/biometrics.ts
import * as LocalAuthentication from "expo-local-authentication";
export async function authenticateWithBiometrics(): Promise<boolean> {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
if (!hasHardware) return false;
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Authenticate to continue",
fallbackLabel: "Use passcode",
disableDeviceFallback: false,
});
return result.success;
}
// services/notifications.ts
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
import Constants from "expo-constants";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications() {
let token: string | undefined;
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
});
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
return null;
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
return token;
}
```
### Pattern 5: Platform-Specific Code
```typescript
// components/ui/Button.tsx
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.value }],
}))
const handlePressIn = () => {
scale.value = withSpring(0.95)
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
}
}
const handlePressOut = () => {
scale.value = withSpring(1)
}
return (
<AnimatedPressable
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled}
style={[
styles.button,
styles[variant],
disabled && styles.disabled,
animatedStyle,
]}
>
<Text style={[styles.text, styles[`${variant}Text`]]}>{title}</Text>
</AnimatedPressable>
)
}
// Platform-specific files
// Button.ios.tsx - iOS-specific implementation
// Button.android.tsx - Android-specific implementation
// Button.web.tsx - Web-specific implementation
// Or use Platform.select
const styles = StyleSheet.create({
button: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
primary: {
backgroundColor: '#007AFF',
},
secondary: {
backgroundColor: '#5856D6',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#007AFF',
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
fontWeight: '600',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#FFFFFF',
},
outlineText: {
color: '#007AFF',
},
})
```
### Pattern 6: Performance Optimization
```typescript
// components/ProductList.tsx
import { FlashList } from '@shopify/flash-list'
import { memo, useCallback } from 'react'
interface ProductListProps {
products: Product[]
onProductPress: (id: string) => void
}
// Memoize list item
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={styles.title}>{item.name}</Text>
<Text style={styles.price}>${item.price}</Text>
</Pressable>
)
})
export function ProductList({ products, onProductPress }: ProductListProps) {
const renderItem = useCallback(
({ item }: { item: Product }) => (
<ProductItem item={item} onPress={onProductPress} />
),
[onProductPress]
)
const keyExtractor = useCallback((item: Product) => item.id, [])
return (
<FlashList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={100}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
// Pull to refresh
onRefresh={onRefresh}
refreshing={isRefreshing}
/>
)
}
```
## EAS Build & Submit
```json
// eas.json
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": { "appleId": "your@email.com", "ascAppId": "123456789" },
"android": { "serviceAccountKeyPath": "./google-services.json" }
}
}
}
```
```bash
# Build commands
eas build --platform ios --profile development
eas build --platform android --profile preview
eas build --platform all --profile production
# Submit to stores
eas submit --platform ios
eas submit --platform android
# OTA updates
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 unforgivingRelated Skills
More skills in Mobile App Development
Android Design Guidelines
Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.
Android Java Skill
Android Java development with MVVM, ViewBinding, and Espresso testing
Android Kotlin
Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing
Appium Skill
Generates production-grade Appium mobile automation scripts for Android and iOS in Java, Python, or JavaScript. Supports real device and emulator testing locally and on TestMu AI cloud with 100+ real devices. Use when the user asks to automate mobile apps, test on Android/iOS, write Appium tests, or mentions "Appium", "mobile testing", "real device", "app automation". Triggers on: "Appium", "mobile test", "Android test", "iOS test", "real device", "app automation", "UiAutomator", "XCUITest driver", "TestMu", "LambdaTest".
Apple Appstore Reviewer
Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.
App Rejection Recovery
When the user's app or update was rejected by Apple App Review or Google Play Review and they need to diagnose why, fix it, and resubmit fast. Use when the user mentions "app rejected", "App Review rejection", "guideline violation", "Apple rejected my app", "Google Play rejected", "Play policy violation", "Resolution Center", "metadata rejection", "binary rejection", "guideline 2.1", "guideline 4.3", "guideline 5.1.1", "Sign in with Apple required", "Apple ID rejection", "Play Store suspension", "appeal", "I need to respond to App Review", or "expedited review". For pre-submission listing health, see aso-audit. For metadata-only fixes, see metadata-optimization.
Explore Other Categories
Skills from other categories with shared topics
.NET Backend Development Patterns
Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.
API Design Principles
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards.
Authentication & Authorization Implementation Patterns
Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.