React Native Architecture
Build production React Native apps with modern architecture patterns
✨ The solution you've been looking for
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.
See It In Action
Interactive preview & real-world examples
AI Conversation Simulator
See how users interact with this skill
User Prompt
Help me set up a new React Native app with Expo Router, authentication, and offline data sync for a shopping app
Skill Processing
Analyzing request...
Agent Response
Complete project structure with navigation setup, auth provider, offline-first data handling, and TypeScript configuration
Quick Start (3 Steps)
Get up and running in minutes
Install
claude-code skill install react-native-architecture
claude-code skill install react-native-architectureConfig
First Trigger
@react-native-architecture helpCommands
| Command | Description | Required Args |
|---|---|---|
| @react-native-architecture setup-new-react-native-project | Create a production-ready React Native project with Expo Router, authentication flow, and offline capabilities | None |
| @react-native-architecture implement-complex-navigation | Build nested navigation patterns with tabs, stacks, and modals using Expo Router | None |
| @react-native-architecture optimize-app-performance | Apply performance optimization techniques including list virtualization, memoization, and native animations | None |
Typical Use Cases
Setup New React Native Project
Create a production-ready React Native project with Expo Router, authentication flow, and offline capabilities
Implement Complex Navigation
Build nested navigation patterns with tabs, stacks, and modals using Expo Router
Optimize App Performance
Apply performance optimization techniques including list virtualization, memoization, and native animations
Overview
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
1# Create new Expo project
2npx create-expo-app@latest my-app -t expo-template-blank-typescript
3
4# Install essential dependencies
5npx expo install expo-router expo-status-bar react-native-safe-area-context
6npx expo install @react-native-async-storage/async-storage
7npx expo install expo-secure-store expo-haptics
1// app/_layout.tsx
2import { Stack } from 'expo-router'
3import { ThemeProvider } from '@/providers/ThemeProvider'
4import { QueryProvider } from '@/providers/QueryProvider'
5
6export default function RootLayout() {
7 return (
8 <QueryProvider>
9 <ThemeProvider>
10 <Stack screenOptions={{ headerShown: false }}>
11 <Stack.Screen name="(tabs)" />
12 <Stack.Screen name="(auth)" />
13 <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
14 </Stack>
15 </ThemeProvider>
16 </QueryProvider>
17 )
18}
Patterns
Pattern 1: Expo Router Navigation
1// app/(tabs)/_layout.tsx
2import { Tabs } from 'expo-router'
3import { Home, Search, User, Settings } from 'lucide-react-native'
4import { useTheme } from '@/hooks/useTheme'
5
6export default function TabLayout() {
7 const { colors } = useTheme()
8
9 return (
10 <Tabs
11 screenOptions={{
12 tabBarActiveTintColor: colors.primary,
13 tabBarInactiveTintColor: colors.textMuted,
14 tabBarStyle: { backgroundColor: colors.background },
15 headerShown: false,
16 }}
17 >
18 <Tabs.Screen
19 name="index"
20 options={{
21 title: 'Home',
22 tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
23 }}
24 />
25 <Tabs.Screen
26 name="search"
27 options={{
28 title: 'Search',
29 tabBarIcon: ({ color, size }) => <Search size={size} color={color} />,
30 }}
31 />
32 <Tabs.Screen
33 name="profile"
34 options={{
35 title: 'Profile',
36 tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
37 }}
38 />
39 <Tabs.Screen
40 name="settings"
41 options={{
42 title: 'Settings',
43 tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
44 }}
45 />
46 </Tabs>
47 )
48}
49
50// app/(tabs)/profile/[id].tsx - Dynamic route
51import { useLocalSearchParams } from 'expo-router'
52
53export default function ProfileScreen() {
54 const { id } = useLocalSearchParams<{ id: string }>()
55
56 return <UserProfile userId={id} />
57}
58
59// Navigation from anywhere
60import { router } from 'expo-router'
61
62// Programmatic navigation
63router.push('/profile/123')
64router.replace('/login')
65router.back()
66
67// With params
68router.push({
69 pathname: '/product/[id]',
70 params: { id: '123', referrer: 'home' },
71})
Pattern 2: Authentication Flow
1// providers/AuthProvider.tsx
2import { createContext, useContext, useEffect, useState } from 'react'
3import { useRouter, useSegments } from 'expo-router'
4import * as SecureStore from 'expo-secure-store'
5
6interface AuthContextType {
7 user: User | null
8 isLoading: boolean
9 signIn: (credentials: Credentials) => Promise<void>
10 signOut: () => Promise<void>
11}
12
13const AuthContext = createContext<AuthContextType | null>(null)
14
15export function AuthProvider({ children }: { children: React.ReactNode }) {
16 const [user, setUser] = useState<User | null>(null)
17 const [isLoading, setIsLoading] = useState(true)
18 const segments = useSegments()
19 const router = useRouter()
20
21 // Check authentication on mount
22 useEffect(() => {
23 checkAuth()
24 }, [])
25
26 // Protect routes
27 useEffect(() => {
28 if (isLoading) return
29
30 const inAuthGroup = segments[0] === '(auth)'
31
32 if (!user && !inAuthGroup) {
33 router.replace('/login')
34 } else if (user && inAuthGroup) {
35 router.replace('/(tabs)')
36 }
37 }, [user, segments, isLoading])
38
39 async function checkAuth() {
40 try {
41 const token = await SecureStore.getItemAsync('authToken')
42 if (token) {
43 const userData = await api.getUser(token)
44 setUser(userData)
45 }
46 } catch (error) {
47 await SecureStore.deleteItemAsync('authToken')
48 } finally {
49 setIsLoading(false)
50 }
51 }
52
53 async function signIn(credentials: Credentials) {
54 const { token, user } = await api.login(credentials)
55 await SecureStore.setItemAsync('authToken', token)
56 setUser(user)
57 }
58
59 async function signOut() {
60 await SecureStore.deleteItemAsync('authToken')
61 setUser(null)
62 }
63
64 if (isLoading) {
65 return <SplashScreen />
66 }
67
68 return (
69 <AuthContext.Provider value={{ user, isLoading, signIn, signOut }}>
70 {children}
71 </AuthContext.Provider>
72 )
73}
74
75export const useAuth = () => {
76 const context = useContext(AuthContext)
77 if (!context) throw new Error('useAuth must be used within AuthProvider')
78 return context
79}
Pattern 3: Offline-First with React Query
1// providers/QueryProvider.tsx
2import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
4import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
5import AsyncStorage from '@react-native-async-storage/async-storage'
6import NetInfo from '@react-native-community/netinfo'
7import { onlineManager } from '@tanstack/react-query'
8
9// Sync online status
10onlineManager.setEventListener((setOnline) => {
11 return NetInfo.addEventListener((state) => {
12 setOnline(!!state.isConnected)
13 })
14})
15
16const queryClient = new QueryClient({
17 defaultOptions: {
18 queries: {
19 gcTime: 1000 * 60 * 60 * 24, // 24 hours
20 staleTime: 1000 * 60 * 5, // 5 minutes
21 retry: 2,
22 networkMode: 'offlineFirst',
23 },
24 mutations: {
25 networkMode: 'offlineFirst',
26 },
27 },
28})
29
30const asyncStoragePersister = createAsyncStoragePersister({
31 storage: AsyncStorage,
32 key: 'REACT_QUERY_OFFLINE_CACHE',
33})
34
35export function QueryProvider({ children }: { children: React.ReactNode }) {
36 return (
37 <PersistQueryClientProvider
38 client={queryClient}
39 persistOptions={{ persister: asyncStoragePersister }}
40 >
41 {children}
42 </PersistQueryClientProvider>
43 )
44}
45
46// hooks/useProducts.ts
47import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
48
49export function useProducts() {
50 return useQuery({
51 queryKey: ['products'],
52 queryFn: api.getProducts,
53 // Use stale data while revalidating
54 placeholderData: (previousData) => previousData,
55 })
56}
57
58export function useCreateProduct() {
59 const queryClient = useQueryClient()
60
61 return useMutation({
62 mutationFn: api.createProduct,
63 // Optimistic update
64 onMutate: async (newProduct) => {
65 await queryClient.cancelQueries({ queryKey: ['products'] })
66 const previous = queryClient.getQueryData(['products'])
67
68 queryClient.setQueryData(['products'], (old: Product[]) => [
69 ...old,
70 { ...newProduct, id: 'temp-' + Date.now() },
71 ])
72
73 return { previous }
74 },
75 onError: (err, newProduct, context) => {
76 queryClient.setQueryData(['products'], context?.previous)
77 },
78 onSettled: () => {
79 queryClient.invalidateQueries({ queryKey: ['products'] })
80 },
81 })
82}
Pattern 4: Native Module Integration
1// services/haptics.ts
2import * as Haptics from "expo-haptics";
3import { Platform } from "react-native";
4
5export const haptics = {
6 light: () => {
7 if (Platform.OS !== "web") {
8 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
9 }
10 },
11 medium: () => {
12 if (Platform.OS !== "web") {
13 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
14 }
15 },
16 heavy: () => {
17 if (Platform.OS !== "web") {
18 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
19 }
20 },
21 success: () => {
22 if (Platform.OS !== "web") {
23 Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
24 }
25 },
26 error: () => {
27 if (Platform.OS !== "web") {
28 Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
29 }
30 },
31};
32
33// services/biometrics.ts
34import * as LocalAuthentication from "expo-local-authentication";
35
36export async function authenticateWithBiometrics(): Promise<boolean> {
37 const hasHardware = await LocalAuthentication.hasHardwareAsync();
38 if (!hasHardware) return false;
39
40 const isEnrolled = await LocalAuthentication.isEnrolledAsync();
41 if (!isEnrolled) return false;
42
43 const result = await LocalAuthentication.authenticateAsync({
44 promptMessage: "Authenticate to continue",
45 fallbackLabel: "Use passcode",
46 disableDeviceFallback: false,
47 });
48
49 return result.success;
50}
51
52// services/notifications.ts
53import * as Notifications from "expo-notifications";
54import { Platform } from "react-native";
55import Constants from "expo-constants";
56
57Notifications.setNotificationHandler({
58 handleNotification: async () => ({
59 shouldShowAlert: true,
60 shouldPlaySound: true,
61 shouldSetBadge: true,
62 }),
63});
64
65export async function registerForPushNotifications() {
66 let token: string | undefined;
67
68 if (Platform.OS === "android") {
69 await Notifications.setNotificationChannelAsync("default", {
70 name: "default",
71 importance: Notifications.AndroidImportance.MAX,
72 vibrationPattern: [0, 250, 250, 250],
73 });
74 }
75
76 const { status: existingStatus } = await Notifications.getPermissionsAsync();
77 let finalStatus = existingStatus;
78
79 if (existingStatus !== "granted") {
80 const { status } = await Notifications.requestPermissionsAsync();
81 finalStatus = status;
82 }
83
84 if (finalStatus !== "granted") {
85 return null;
86 }
87
88 const projectId = Constants.expoConfig?.extra?.eas?.projectId;
89 token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
90
91 return token;
92}
Pattern 5: Platform-Specific Code
1// components/ui/Button.tsx
2import { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native'
3import * as Haptics from 'expo-haptics'
4import Animated, {
5 useAnimatedStyle,
6 useSharedValue,
7 withSpring,
8} from 'react-native-reanimated'
9
10const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
11
12interface ButtonProps {
13 title: string
14 onPress: () => void
15 variant?: 'primary' | 'secondary' | 'outline'
16 disabled?: boolean
17}
18
19export function Button({
20 title,
21 onPress,
22 variant = 'primary',
23 disabled = false,
24}: ButtonProps) {
25 const scale = useSharedValue(1)
26
27 const animatedStyle = useAnimatedStyle(() => ({
28 transform: [{ scale: scale.value }],
29 }))
30
31 const handlePressIn = () => {
32 scale.value = withSpring(0.95)
33 if (Platform.OS !== 'web') {
34 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
35 }
36 }
37
38 const handlePressOut = () => {
39 scale.value = withSpring(1)
40 }
41
42 return (
43 <AnimatedPressable
44 onPress={onPress}
45 onPressIn={handlePressIn}
46 onPressOut={handlePressOut}
47 disabled={disabled}
48 style={[
49 styles.button,
50 styles[variant],
51 disabled && styles.disabled,
52 animatedStyle,
53 ]}
54 >
55 <Text style={[styles.text, styles[`${variant}Text`]]}>{title}</Text>
56 </AnimatedPressable>
57 )
58}
59
60// Platform-specific files
61// Button.ios.tsx - iOS-specific implementation
62// Button.android.tsx - Android-specific implementation
63// Button.web.tsx - Web-specific implementation
64
65// Or use Platform.select
66const styles = StyleSheet.create({
67 button: {
68 paddingVertical: 12,
69 paddingHorizontal: 24,
70 borderRadius: 8,
71 alignItems: 'center',
72 ...Platform.select({
73 ios: {
74 shadowColor: '#000',
75 shadowOffset: { width: 0, height: 2 },
76 shadowOpacity: 0.1,
77 shadowRadius: 4,
78 },
79 android: {
80 elevation: 4,
81 },
82 }),
83 },
84 primary: {
85 backgroundColor: '#007AFF',
86 },
87 secondary: {
88 backgroundColor: '#5856D6',
89 },
90 outline: {
91 backgroundColor: 'transparent',
92 borderWidth: 1,
93 borderColor: '#007AFF',
94 },
95 disabled: {
96 opacity: 0.5,
97 },
98 text: {
99 fontSize: 16,
100 fontWeight: '600',
101 },
102 primaryText: {
103 color: '#FFFFFF',
104 },
105 secondaryText: {
106 color: '#FFFFFF',
107 },
108 outlineText: {
109 color: '#007AFF',
110 },
111})
Pattern 6: Performance Optimization
1// components/ProductList.tsx
2import { FlashList } from '@shopify/flash-list'
3import { memo, useCallback } from 'react'
4
5interface ProductListProps {
6 products: Product[]
7 onProductPress: (id: string) => void
8}
9
10// Memoize list item
11const ProductItem = memo(function ProductItem({
12 item,
13 onPress,
14}: {
15 item: Product
16 onPress: (id: string) => void
17}) {
18 const handlePress = useCallback(() => onPress(item.id), [item.id, onPress])
19
20 return (
21 <Pressable onPress={handlePress} style={styles.item}>
22 <FastImage
23 source={{ uri: item.image }}
24 style={styles.image}
25 resizeMode="cover"
26 />
27 <Text style={styles.title}>{item.name}</Text>
28 <Text style={styles.price}>${item.price}</Text>
29 </Pressable>
30 )
31})
32
33export function ProductList({ products, onProductPress }: ProductListProps) {
34 const renderItem = useCallback(
35 ({ item }: { item: Product }) => (
36 <ProductItem item={item} onPress={onProductPress} />
37 ),
38 [onProductPress]
39 )
40
41 const keyExtractor = useCallback((item: Product) => item.id, [])
42
43 return (
44 <FlashList
45 data={products}
46 renderItem={renderItem}
47 keyExtractor={keyExtractor}
48 estimatedItemSize={100}
49 // Performance optimizations
50 removeClippedSubviews={true}
51 maxToRenderPerBatch={10}
52 windowSize={5}
53 // Pull to refresh
54 onRefresh={onRefresh}
55 refreshing={isRefreshing}
56 />
57 )
58}
EAS Build & Submit
1// eas.json
2{
3 "cli": { "version": ">= 5.0.0" },
4 "build": {
5 "development": {
6 "developmentClient": true,
7 "distribution": "internal",
8 "ios": { "simulator": true }
9 },
10 "preview": {
11 "distribution": "internal",
12 "android": { "buildType": "apk" }
13 },
14 "production": {
15 "autoIncrement": true
16 }
17 },
18 "submit": {
19 "production": {
20 "ios": { "appleId": "your@email.com", "ascAppId": "123456789" },
21 "android": { "serviceAccountKeyPath": "./google-services.json" }
22 }
23 }
24}
1# Build commands
2eas build --platform ios --profile development
3eas build --platform android --profile preview
4eas build --platform all --profile production
5
6# Submit to stores
7eas submit --platform ios
8eas submit --platform android
9
10# OTA updates
11eas 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
What Users Are Saying
Real feedback from the community
Environment Matrix
Dependencies
Framework Support
Context Window
Security & Privacy
Information
- Author
- wshobson
- Updated
- 2026-01-30
- Category
- cms-platforms
Related Skills
React Native Architecture
Build production React Native apps with Expo, navigation, native modules, offline sync, and …
View Details →Angular Migration
Migrate from AngularJS to Angular using hybrid mode, incremental component rewriting, and dependency …
View Details →Angular Migration
Migrate from AngularJS to Angular using hybrid mode, incremental component rewriting, and dependency …
View Details →