react-native-architecture

安装量: 4.9K
排名: #595

安装

npx skills add https://github.com/wshobson/agents --skill 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 ( ) }

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 ( , }} /> , }} /> , }} /> , }} /> ) }

// app/(tabs)/profile/[id].tsx - Dynamic route import { useLocalSearchParams } from 'expo-router'

export default function ProfileScreen() { const { id } = useLocalSearchParams<{ id: string }>()

return }

// 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 signOut: () => Promise }

const AuthContext = createContext(null)

export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(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 }

return ( {children} ) }

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 ( {children} ) }

// 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 { 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 ( ${variant}Text]]}>{title} ) }

// 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 ( {item.name} ${item.price} ) })

export function ProductList({ products, onProductPress }: ProductListProps) { const renderItem = useCallback( ({ item }: { item: Product }) => ( ), [onProductPress] )

const keyExtractor = useCallback((item: Product) => item.id, [])

return ( ) }

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 Resources Expo Documentation Expo Router React Native Performance FlashList

返回排行榜