React Native Architecture

Build production React Native apps with modern architecture patterns

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

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 expo mobile-development navigation offline-sync native-modules cross-platform typescript
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

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

1

Install

claude-code skill install react-native-architecture

claude-code skill install react-native-architecture
2

Config

3

First Trigger

@react-native-architecture help

Commands

CommandDescriptionRequired Args
@react-native-architecture setup-new-react-native-projectCreate a production-ready React Native project with Expo Router, authentication flow, and offline capabilitiesNone
@react-native-architecture implement-complex-navigationBuild nested navigation patterns with tabs, stacks, and modals using Expo RouterNone
@react-native-architecture optimize-app-performanceApply performance optimization techniques including list virtualization, memoization, and native animationsNone

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

FeatureExpoBare RN
Setup complexityLowHigh
Native modulesEAS BuildManual linking
OTA updatesBuilt-inManual setup
Build serviceEASCustom CI
Custom native codeConfig pluginsDirect 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

Node.js 16+
Expo CLI
React Native 0.72+
TypeScript 4.9+

Framework Support

Expo ✓ (recommended) React Native CLI ✓ Expo Router ✓ React Query ✓ React Native Reanimated ✓

Context Window

Token Usage ~3K-8K tokens for complex navigation and component patterns

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
cms-platforms