skills/sivag-lab/roth_mcp/react-native-architecture

react-native-architecture

SKILL.md

React Native Architecture

Production patterns for React Native with Expo, covering project setup, navigation, state management, native integration, offline-first, performance, and CI/CD.

When to Use

  • 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

Project Setup (Expo)

# Create new Expo project
npx create-expo-app@latest my-app --template tabs
cd my-app

# Or with blank TypeScript template
npx create-expo-app@latest my-app -t expo-template-blank-typescript

Recommended Project Structure

src/
  app/                  # Expo Router file-based routes
    (tabs)/             # Tab navigator group
      index.tsx         # Home tab
      profile.tsx       # Profile tab
    _layout.tsx         # Root layout
    +not-found.tsx      # 404 screen
  components/           # Shared UI components
    ui/                 # Primitives (Button, Input, Card)
  hooks/                # Custom hooks
  lib/                  # Utilities, API client, storage
  store/                # State management (Zustand)
  constants/            # Theme, config, enums
  types/                # TypeScript definitions

Navigation

Expo Router (Recommended)

File-based routing, similar to Next.js.

// app/_layout.tsx — Root layout
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
    </Stack>
  );
}

// app/(tabs)/_layout.tsx — Tab navigator
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{ title: 'Profile' }}
      />
    </Tabs>
  );
}

Navigation Patterns

Pattern Implementation
Stack <Stack> in layout — push/pop screens
Tabs <Tabs> in layout — bottom tab bar
Drawer expo-router with drawer layout
Modal presentation: 'modal' in screen options
Deep linking Automatic with Expo Router file paths
Auth flow Conditional layout based on auth state

Auth-Protected Routes

// app/_layout.tsx
import { Redirect } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';

export default function RootLayout() {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) return <SplashScreen />;
  if (!isAuthenticated) return <Redirect href="/login" />;

  return <Stack />;
}

State Management

Scope Solution
Component useState, useReducer
Server data TanStack Query (React Query)
Global UI Zustand
Persistent MMKV + Zustand persist
Forms React Hook Form

Zustand Store

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { zustandStorage } from '@/lib/mmkv-storage';

interface AppStore {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
}

export const useAppStore = create<AppStore>()(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'app-store', storage: createJSONStorage(() => zustandStorage) }
  )
);

TanStack Query for API Data

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => api.getUser(id),
    staleTime: 5 * 60 * 1000,
  });
}

function useUpdateUser() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: api.updateUser,
    onSuccess: (_, vars) => qc.invalidateQueries({ queryKey: ['user', vars.id] }),
  });
}

Native Modules & Platform APIs

Using Expo Modules

npx expo install expo-camera expo-location expo-notifications expo-file-system
import * as Location from 'expo-location';

async function getLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') throw new Error('Permission denied');
  return Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
}

Platform-Specific Code

import { Platform } from 'react-native';

const styles = {
  shadow: Platform.select({
    ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 },
    android: { elevation: 4 },
  }),
};

// File-based: MyComponent.ios.tsx / MyComponent.android.tsx

Offline-First Architecture

Storage Options

Solution Use For Speed
MMKV Key-value, small data, preferences Fastest
SQLite (expo-sqlite) Structured data, queries, relations Fast
AsyncStorage Legacy key-value (avoid for new) Slow
FileSystem Large files, downloads, cache Varies

Offline Queue Pattern

import NetInfo from '@react-native-community/netinfo';

class OfflineQueue {
  private queue: PendingAction[] = [];

  async enqueue(action: PendingAction) {
    this.queue.push(action);
    await this.persist();
    this.processIfOnline();
  }

  private async processIfOnline() {
    const { isConnected } = await NetInfo.fetch();
    if (!isConnected) return;

    while (this.queue.length > 0) {
      const action = this.queue[0];
      try {
        await this.execute(action);
        this.queue.shift();
        await this.persist();
      } catch {
        break; // Retry later
      }
    }
  }
}

Performance

Optimization Checklist

Area Action
Lists Use FlashList instead of FlatList
Images Use expo-image (caching, blurhash, transitions)
Animations Use react-native-reanimated (UI thread)
Heavy computation Move to worklet or background thread
Re-renders Profile with React DevTools, apply memo
Bundle Use Hermes engine (default in Expo 49+)
Startup Minimize root component, lazy load screens

FlashList (Drop-in FlatList Replacement)

import { FlashList } from '@shopify/flash-list';

<FlashList
  data={items}
  renderItem={({ item }) => <ItemCard item={item} />}
  estimatedItemSize={80}
  keyExtractor={(item) => item.id}
/>

Image Optimization

import { Image } from 'expo-image';

<Image
  source={{ uri: imageUrl }}
  placeholder={{ blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH' }}
  contentFit="cover"
  transition={200}
  style={{ width: 200, height: 200 }}
/>

CI/CD with EAS Build

Setup

npm install -g eas-cli
eas login
eas build:configure

eas.json

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "ios": { "simulator": true }
    },
    "production": {}
  },
  "submit": {
    "production": {
      "ios": { "appleId": "you@example.com", "ascAppId": "123456789" },
      "android": { "serviceAccountKeyPath": "./google-services.json" }
    }
  }
}

Build Commands

eas build --platform ios --profile preview     # iOS simulator build
eas build --platform android --profile preview # Android APK
eas build --platform all --profile production  # Production builds
eas submit --platform all                       # Submit to stores
eas update --branch preview --message "Bug fix" # OTA update

Project Checklist

  • Expo SDK latest (or target version)
  • TypeScript configured with strict mode
  • Expo Router for navigation
  • Zustand + MMKV for state persistence
  • TanStack Query for server state
  • FlashList for performant lists
  • expo-image for optimized images
  • Reanimated for animations
  • EAS Build configured for CI/CD
  • Deep linking tested
  • Offline handling implemented
  • Platform-specific code isolated
  • Error boundaries at route level
Weekly Installs
3
GitHub Stars
1
First Seen
5 days ago
Installed on
opencode3
claude-code3
github-copilot3
codex3
kimi-cli3
gemini-cli3