react-native-architecture

React Native Architecture

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-native-architecture" with this command: npx skills add wshobson/agents/wshobson-agents-react-native-architecture

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

  1. 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

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

// 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

// 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

// 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 &#x26;&#x26; !inAuthGroup) {
  router.replace('/login')
} else if (user &#x26;&#x26; 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

// 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

// 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

// 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

// 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

// 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" } } } }

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 unforgiving

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

tailwind-design-system

Tailwind Design System (v4)

Repository Source
19.1K31.3Kwshobson
Automation

api-design-principles

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

nodejs-backend-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

nextjs-app-router-patterns

No summary provided by upstream source.

Repository SourceNeeds Review