Claude Cursor Skill

react-native-architecture

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.

LLM Mart · 0 points · 13 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wshobson-agents-plugins_frontend-mobile-development_skills_react-native-architecture-554237f.zip · 6 KB
Part of wshobson/agents — 170 skills

Install

skills CLI npx skills add https://github.com/wshobson/agents/tree/main/plugins/frontend-mobile-development/skills/react-native-architecture
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
Git git clone https://github.com/wshobson/agents.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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

# 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>
  )
}

Detailed patterns and worked examples

Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.

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
Files (agents)
  • references
    • details.md 13.4 KB
      # react-native-architecture — detailed patterns and worked examples
      
      ## 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"
      ```
      
  • SKILL.md 3.5 KB
    ---
    name: react-native-architecture
    description: 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 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>
      )
    }
    ```
    
    ## Detailed patterns and worked examples
    
    Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
    
    ## 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
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related