react-native-architecture
upstream
backend

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.

ID: react-native-architecture
Preview
Codigo

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 unforgiving

Resources


  • [Expo Documentation](https://docs.expo.dev/)
  • [Expo Router](https://docs.expo.dev/router/introduction/)
  • [React Native Performance](https://reactnative.dev/docs/performance)
  • [FlashList](https://shopify.github.io/flash-list/)

Informacion

Estado
Activo
Origen upstream
Usos 3
Ultimo uso 23/01 16:57
Actualizado 09/01/2026 02:14

Archivo origen

Z:\Repositorios\core\upstream\plugins\frontend-mobile-development\skills\react-native-architecture\SKILL.md