---
name: react-native
description: Build production React Native apps with Expo SDK 53+, Expo Router (file-based navigation), New Architecture (Fabric + TurboModules), FlashList, Reanimated 4, Zustand for state, Hermes, EAS Build, and App Store/Play Store deployment.
triggers:
- "create a React Native app"
- "set up navigation"
- "optimize list performance"
- "handle deep links"
- "configure push notifications"
- "deploy to stores"
- "Expo Router"
- "FlashList"
- "Reanimated"
- "Zustand"
- "React Navigation"
- "EAS Build"
- "react native"
- "expo"
- "mobile app"
- "iOS"
- "Android"
- "react-navigation"
- "expo router"
- "native module"
negatives:
- "Flutter"
- "web-only React"
- "PWA"
- "mobile web"
- "Ionic"
- "Cordova"
- "Capacitor"
- "Xamarin"
license: MIT
compatibility: opencode
metadata:
workflow: mobile
audience: developers
version: "3.0.0"
allowed-tools:
- bash
- edit
- glob
- grep
- read
- write
- websearch
- webfetch
React Native Architect
Build production React Native apps with Expo, New Architecture, and modern navigation.
Workflow
- Scaffold —
npx create-expo-app@latest MyApp --template blank-typescript, configure SDK 53+
- Navigation — Choose between Expo Router (greenfield Expo) or React Navigation v7 (bare RN / brownfield). Set up layouts, typed routes, deep linking
- State management — Zustand for local client state, TanStack Query for server state. Avoid Redux unless dealing with massive synchronous state trees
- New Architecture — Ensure Fabric + TurboModules enabled (default in SDK 53+). Verify third-party lib compatibility
- Lists — Replace all FlatLists with FlashList from Shopify. Configure
estimatedItemSize. Optimize renderItem with React.memo
- Animations — Reanimated 4 for gesture-driven animations. Use
useSharedValue + useAnimatedStyle instead of Animated API
- Images —
expo-image with cached, resized variants. Never bare <Image> without dimensions
- Notifications —
expo-notifications for push. Handle foreground, background, tap-to-navigate
- Performance — Enable Hermes, lazy-load tab screens, freeze inactive screens via
react-native-screens, InteractionManager.runAfterInteractions for heavy ops
- Ship —
eas build --platform all --profile production, eas submit, configure CI/CD with GitHub Actions
Navigation Decision
| Scenario |
Recommended |
| Greenfield Expo app |
Expo Router (file-based, typed routes, deep links) |
| Bare React Native |
React Navigation v7 (imperative, full control) |
| Brownfield / native modules |
React Navigation v7 |
| Web + mobile |
Expo Router (SSR, SEO) |
Expo Router (file-based)
app/
├ _layout.tsx # Root layout (tabs, stack)
├ index.tsx # /
├ (tabs)/
│ ├ _layout.tsx # Tab config
│ ├ feed.tsx # /feed
│ └ profile.tsx # /profile
├ product/
│ ├ [id].tsx # /product/:id
│ └ review.tsx # /product/:id/review
└ auth/
├ login.tsx # /auth/login
└ signup.tsx # /auth/signup
Deep links work automatically. Universal links with app.json scheme config.
Typed Routes (Expo Router v4+)
// app/product/[id].tsx
import { useLocalSearchParams } from 'expo-router'
export default function ProductScreen() {
const { id } = useLocalSearchParams<{ id: string }>()
// id is typed as string
}
React Navigation v7
type RootStackParamList = {
Home: undefined
ProductDetail: { id: string }
Cart: undefined
}
const Stack = createNativeStackNavigator<RootStackParamList>()
export function RootNavigator() {
return (
<NavigationContainer
linking={{
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: '',
ProductDetail: 'product/:id',
Cart: 'cart',
},
},
}}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
<Stack.Screen name="Cart" component={CartScreen} />
</Stack.Navigator>
</NavigationContainer>
)
}
State Management
| Scenario |
Recommended |
| Simple, small app |
React Context + useState |
| Medium app |
Zustand (minimal boilerplate, hooks-based) |
| Large app, complex state |
Redux Toolkit (mature ecosystem, DevTools) |
| Server state |
TanStack Query (caching, refetch, pagination) |
Zustand (preferred for most apps)
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface CartStore {
items: CartItem[]
total: number
addItem: (item: Product) => void
removeItem: (id: string) => void
clear: () => void
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }],
total: state.total + product.price,
})),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id),
total: state.items.filter((i) => i.id !== id).reduce((s, i) => s + i.price, 0),
})),
clear: () => set({ items: [], total: 0 }),
}),
{ name: 'cart-storage', storage: createJSONStorage(() => AsyncStorage) }
)
)
Performance Rules
| Rule |
Implementation |
| Enable Hermes |
hermes: true in metro.config.js |
| Use FlashList (Shopify) |
Replaces FlatList — built-in recycling, better perf |
| InteractionManager |
InteractionManager.runAfterInteractions(() => fetchData()) |
| Memoize components |
React.memo on screen components, list items |
| Lazy load tab screens |
lazy: true (default in Expo Router) |
| Image optimization |
expo-image with cached, resized images |
| Avoid inline functions in render |
Extract handlers, use useCallback |
| Freeze inactive screens |
react-native-screens detaches from native hierarchy |
FlashList (preferred over FlatList)
import { FlashList } from '@shopify/flash-list'
<FlashList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
estimatedItemSize={120}
/>
New Architecture
Enabled by default in Expo SDK 53+.
| Component |
Old |
New |
| Native modules |
NativeModules proxy |
TurboModules (typed, synchronous) |
| View manager |
Fabric not used |
Fabric (synchronous rendering) |
| State updates |
Bridge (async serialization) |
JSI (synchronous, no serialization) |
Ensure compatibility:
// app.json
{
"expo": {
"platforms": ["ios", "android"]
}
}
Reanimated 4
import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'
export function AnimatedCard() {
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
return (
<Animated.View
style={animatedStyle}
=> { scale.value = withSpring(0.95) }}
=> { scale.value = withSpring(1) }}
/>
)
}
Deep Linking
{
"expo": {
"scheme": "myapp",
"plugins": [["expo-linking"]]
}
}
Always implement a fallback for malformed links.
Push Notifications
import * as Notifications from 'expo-notifications'
const { status } = await Notifications.requestPermissionsAsync()
if (status !== 'granted') return
const token = await Notifications.getExpoPushTokenAsync()
Notifications.addNotificationResponseReceivedListener((response) => {
const { screen, params } = response.notification.request.content.data
router.push({ pathname: screen as any, params: params as any })
})
Deployment
eas build --platform all --profile production
eas submit --platform ios
eas submit --platform android
Error Handling
| Scenario |
Cause |
Fix |
| White screen on launch |
Unhandled JS exception |
Wrap root with ErrorBoundary, add Sentry |
| Deep link opens wrong screen |
Missing or incorrect route config |
Verify app.json scheme + linking config match |
| Push notification tap does nothing |
No notification response listener |
Register addNotificationResponseReceivedListener |
| FlashList blank / slow |
Missing estimatedItemSize |
Measure or estimate item height, pass to prop |
| Hermes crash on third-party lib |
Lib not Hermes-compatible |
Check lib docs, use Hermes-compatible version |
| EAS build fails on iOS |
Missing provisioning profile |
Run eas credentials, eas device:create |
| AsyncStorage 64KB limit on Android |
Storage exceeds limit |
Migrate to MMKV (react-native-mmkv) |
| Reanimated 4 worklet error |
Using non-worklet code inside useAnimatedStyle |
Only use Reanimated compatible functions inside worklets |
| TurboModule returns undefined |
Fabric/TM not enabled |
Verify newArchEnabled: true in app.json |
| Navigation state lost on restart |
No persistence |
Use persistNavigationState from expo-router or React Navigation v7 storage |
Production Checklist
Anti-Patterns
| Anti-pattern |
Fix |
| ScrollView for long lists |
FlashList with virtualization |
| No Hermes |
Enable Hermes — 2x startup improvement |
| Inline arrow functions in render |
useCallback or extract to component |
| FlatList without optimization |
Use FlashList instead |
| Deep linking as afterthought |
Configure at project start |
| No error boundary |
Unhandled JS error = white screen |
navigation.getParent() chains |
Restructure to flat navigation hierarchy |
setState in navigation handlers |
Navigate first, fetch on screen mount |
Using bare <Image> without dimensions |
expo-image with explicit width/height |
| Zustand store split across 10 files |
Keep logically cohesive, use slices pattern |
| New Architecture disabled by default |
Enable newArchEnabled: true in app.json |
| Manual MethodChannel for native |
Use Expo Modules API (typed, modern) |
| Ignoring platform-specific safe areas |
useSafeAreaInsets from react-native-safe-area-context |
console.log in production builds |
Strip via babel plugin (transform-remove-console) |
Sources
- React Native Documentation (reactnative.dev)
- Expo Documentation (docs.expo.dev)
- Expo Router Documentation
- React Navigation v7 Documentation
- Shopify FlashList (shopify.github.io/flash-list)
- Reanimated 4 Documentation (docs.swmansion.com/react-native-reanimated)
- Hermes Engine Documentation
- EAS Build Documentation
- Zustand Documentation (github.com/pmndrs/zustand)
- TanStack Query Documentation (tanstack.com/query/latest)
Checklist
1---2name: react-native3description: ---4---5---6name: react-native7description: Build production React Native apps with Expo SDK 53+, Expo Router (file-based navigation), New Architecture (Fabric + TurboModules), FlashList, Reanimated 4, Zustand for state, Hermes, EAS Build, and App Store/Play Store deployment.8triggers:9 - "create a React Native app"10 - "set up navigation"11 - "optimize list performance"12 - "handle deep links"13 - "configure push notifications"14 - "deploy to stores"15 - "Expo Router"16 - "FlashList"17 - "Reanimated"18 - "Zustand"19 - "React Navigation"20 - "EAS Build"21 - "react native"22 - "expo"23 - "mobile app"24 - "iOS"25 - "Android"26 - "react-navigation"27 - "expo router"28 - "native module"29negatives:30 - "Flutter"31 - "web-only React"32 - "PWA"33 - "mobile web"34 - "Ionic"35 - "Cordova"36 - "Capacitor"37 - "Xamarin"38license: MIT39compatibility: opencode40metadata:41 workflow: mobile42 audience: developers43 version: "3.0.0"44allowed-tools:45 - bash46 - edit47 - glob48 - grep49 - read50 - write51 - websearch52 - webfetch53---545556# React Native Architect5758Build production React Native apps with Expo, New Architecture, and modern navigation.5960## Workflow61621. **Scaffold** — `npx create-expo-app@latest MyApp --template blank-typescript`, configure SDK 53+632. **Navigation** — Choose between Expo Router (greenfield Expo) or React Navigation v7 (bare RN / brownfield). Set up layouts, typed routes, deep linking643. **State management** — Zustand for local client state, TanStack Query for server state. Avoid Redux unless dealing with massive synchronous state trees654. **New Architecture** — Ensure Fabric + TurboModules enabled (default in SDK 53+). Verify third-party lib compatibility665. **Lists** — Replace all FlatLists with FlashList from Shopify. Configure `estimatedItemSize`. Optimize `renderItem` with `React.memo`676. **Animations** — Reanimated 4 for gesture-driven animations. Use `useSharedValue` + `useAnimatedStyle` instead of `Animated` API687. **Images** — `expo-image` with cached, resized variants. Never bare `<Image>` without dimensions698. **Notifications** — `expo-notifications` for push. Handle foreground, background, tap-to-navigate709. **Performance** — Enable Hermes, lazy-load tab screens, freeze inactive screens via `react-native-screens`, `InteractionManager.runAfterInteractions` for heavy ops7110. **Ship** — `eas build --platform all --profile production`, `eas submit`, configure CI/CD with GitHub Actions7273## Navigation Decision7475| Scenario | Recommended |76|----------|------------|77| Greenfield Expo app | Expo Router (file-based, typed routes, deep links) |78| Bare React Native | React Navigation v7 (imperative, full control) |79| Brownfield / native modules | React Navigation v7 |80| Web + mobile | Expo Router (SSR, SEO) |8182## Expo Router (file-based)8384```85app/86├ _layout.tsx # Root layout (tabs, stack)87├ index.tsx # /88├ (tabs)/89│ ├ _layout.tsx # Tab config90│ ├ feed.tsx # /feed91│ └ profile.tsx # /profile92├ product/93│ ├ [id].tsx # /product/:id94│ └ review.tsx # /product/:id/review95└ auth/96 ├ login.tsx # /auth/login97 └ signup.tsx # /auth/signup98```99100Deep links work automatically. Universal links with `app.json` scheme config.101102### Typed Routes (Expo Router v4+)103104```typescript105// app/product/[id].tsx106import { useLocalSearchParams } from 'expo-router'107108export default function ProductScreen() {109 const { id } = useLocalSearchParams<{ id: string }>()110 // id is typed as string111}112```113114## React Navigation v7115116```typescript117type RootStackParamList = {118 Home: undefined119 ProductDetail: { id: string }120 Cart: undefined121}122123const Stack = createNativeStackNavigator<RootStackParamList>()124125export function RootNavigator() {126 return (127 <NavigationContainer128 linking={{129 prefixes: ['myapp://', 'https://myapp.com'],130 config: {131 screens: {132 Home: '',133 ProductDetail: 'product/:id',134 Cart: 'cart',135 },136 },137 }}>138 <Stack.Navigator screenOptions={{ headerShown: false }}>139 <Stack.Screen name="Home" component={HomeScreen} />140 <Stack.Screen name="ProductDetail" component={ProductDetailScreen} />141 <Stack.Screen name="Cart" component={CartScreen} />142 </Stack.Navigator>143 </NavigationContainer>144 )145}146```147148## State Management149150| Scenario | Recommended |151|----------|-------------|152| Simple, small app | React Context + useState |153| Medium app | Zustand (minimal boilerplate, hooks-based) |154| Large app, complex state | Redux Toolkit (mature ecosystem, DevTools) |155| Server state | TanStack Query (caching, refetch, pagination) |156157### Zustand (preferred for most apps)158159```typescript160import { create } from 'zustand'161import { persist, createJSONStorage } from 'zustand/middleware'162import AsyncStorage from '@react-native-async-storage/async-storage'163164interface CartStore {165 items: CartItem[]166 total: number167 addItem: (item: Product) => void168 removeItem: (id: string) => void169 clear: () => void170}171172export const useCartStore = create<CartStore>()(173 persist(174 (set, get) => ({175 items: [],176 total: 0,177 addItem: (product) => set((state) => ({178 items: [...state.items, { ...product, quantity: 1 }],179 total: state.total + product.price,180 })),181 removeItem: (id) => set((state) => ({182 items: state.items.filter((i) => i.id !== id),183 total: state.items.filter((i) => i.id !== id).reduce((s, i) => s + i.price, 0),184 })),185 clear: () => set({ items: [], total: 0 }),186 }),187 { name: 'cart-storage', storage: createJSONStorage(() => AsyncStorage) }188 )189)190```191192## Performance Rules193194| Rule | Implementation |195|------|---------------|196| Enable Hermes | `hermes: true` in metro.config.js |197| Use FlashList (Shopify) | Replaces FlatList — built-in recycling, better perf |198| InteractionManager | `InteractionManager.runAfterInteractions(() => fetchData())` |199| Memoize components | `React.memo` on screen components, list items |200| Lazy load tab screens | `lazy: true` (default in Expo Router) |201| Image optimization | `expo-image` with cached, resized images |202| Avoid inline functions in render | Extract handlers, use `useCallback` |203| Freeze inactive screens | `react-native-screens` detaches from native hierarchy |204205### FlashList (preferred over FlatList)206207```typescript208import { FlashList } from '@shopify/flash-list'209210<FlashList211 data={items}212 renderItem={renderItem}213 keyExtractor={(item) => item.id}214 estimatedItemSize={120}215/>216```217218## New Architecture219220Enabled by default in Expo SDK 53+.221222| Component | Old | New |223|-----------|-----|-----|224| Native modules | NativeModules proxy | TurboModules (typed, synchronous) |225| View manager | Fabric not used | Fabric (synchronous rendering) |226| State updates | Bridge (async serialization) | JSI (synchronous, no serialization) |227228Ensure compatibility:229```json230// app.json231{232 "expo": {233 "platforms": ["ios", "android"]234 }235}236```237238## Reanimated 4239240```typescript241import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'242243export function AnimatedCard() {244 const scale = useSharedValue(1)245246 const animatedStyle = useAnimatedStyle(() => ({247 transform: [{ scale: scale.value }],248 }))249250 return (251 <Animated.View252 style={animatedStyle}253 onTouchStart={() => { scale.value = withSpring(0.95) }}254 onTouchEnd={() => { scale.value = withSpring(1) }}255 />256 )257}258```259260## Deep Linking261262```json263{264 "expo": {265 "scheme": "myapp",266 "plugins": [["expo-linking"]]267 }268}269```270271Always implement a fallback for malformed links.272273## Push Notifications274275```typescript276import * as Notifications from 'expo-notifications'277278const { status } = await Notifications.requestPermissionsAsync()279if (status !== 'granted') return280281const token = await Notifications.getExpoPushTokenAsync()282283Notifications.addNotificationResponseReceivedListener((response) => {284 const { screen, params } = response.notification.request.content.data285 router.push({ pathname: screen as any, params: params as any })286})287```288289## Deployment290291```bash292eas build --platform all --profile production293eas submit --platform ios294eas submit --platform android295```296297## Error Handling298299| Scenario | Cause | Fix |300|----------|-------|-----|301| White screen on launch | Unhandled JS exception | Wrap root with `ErrorBoundary`, add Sentry |302| Deep link opens wrong screen | Missing or incorrect route config | Verify `app.json` scheme + linking config match |303| Push notification tap does nothing | No notification response listener | Register `addNotificationResponseReceivedListener` |304| FlashList blank / slow | Missing `estimatedItemSize` | Measure or estimate item height, pass to prop |305| Hermes crash on third-party lib | Lib not Hermes-compatible | Check lib docs, use Hermes-compatible version |306| EAS build fails on iOS | Missing provisioning profile | Run `eas credentials`, `eas device:create` |307| AsyncStorage 64KB limit on Android | Storage exceeds limit | Migrate to MMKV (react-native-mmkv) |308| Reanimated 4 worklet error | Using non-worklet code inside `useAnimatedStyle` | Only use Reanimated compatible functions inside worklets |309| TurboModule returns undefined | Fabric/TM not enabled | Verify `newArchEnabled: true` in app.json |310| Navigation state lost on restart | No persistence | Use `persistNavigationState` from expo-router or React Navigation v7 storage |311312## Production Checklist313314- [ ] Hermes engine enabled315- [ ] FlashList (not FlatList) for all lists316- [ ] Images use `expo-image` with resize317- [ ] `React.memo` on list items and screens318- [ ] Navigation screens lazy-loaded319- [ ] Deep linking configured and tested320- [ ] Push notifications configured321- [ ] MMKV or AsyncStorage for persistence322- [ ] Error boundary wrapping root navigator323- [ ] Sentry or similar crash reporting324- [ ] EAS Build for CI/CD325- [ ] New Architecture verified (no TurboModule issues)326- [ ] App Store / Play Store screenshots and metadata327- [ ] `InteractionManager.runAfterInteractions` for heavy operations328- [ ] `expo-constants` env segregation (dev / staging / prod)329330## Anti-Patterns331332| Anti-pattern | Fix |333|-------------|-----|334| ScrollView for long lists | FlashList with virtualization |335| No Hermes | Enable Hermes — 2x startup improvement |336| Inline arrow functions in render | `useCallback` or extract to component |337| FlatList without optimization | Use FlashList instead |338| Deep linking as afterthought | Configure at project start |339| No error boundary | Unhandled JS error = white screen |340| `navigation.getParent()` chains | Restructure to flat navigation hierarchy |341| `setState` in navigation handlers | Navigate first, fetch on screen mount |342| Using bare `<Image>` without dimensions | `expo-image` with explicit width/height |343| Zustand store split across 10 files | Keep logically cohesive, use slices pattern |344| New Architecture disabled by default | Enable `newArchEnabled: true` in app.json |345| Manual MethodChannel for native | Use Expo Modules API (typed, modern) |346| Ignoring platform-specific safe areas | `useSafeAreaInsets` from react-native-safe-area-context |347| `console.log` in production builds | Strip via babel plugin (`transform-remove-console`) |348349## Sources350351- React Native Documentation (reactnative.dev)352- Expo Documentation (docs.expo.dev)353- Expo Router Documentation354- React Navigation v7 Documentation355- Shopify FlashList (shopify.github.io/flash-list)356- Reanimated 4 Documentation (docs.swmansion.com/react-native-reanimated)357- Hermes Engine Documentation358- EAS Build Documentation359- Zustand Documentation (github.com/pmndrs/zustand)360- TanStack Query Documentation (tanstack.com/query/latest)361362## Checklist363364- [ ] Skill loads without errors in the AI agent365- [ ] YAML frontmatter is valid (description, compatibility, audience)366- [ ] Workflow section provides clear step-by-step instructions367- [ ] Error handling section covers common failure modes368- [ ] All referenced files (references/, scripts/, assets/) exist369- [ ] Skill triggers correctly for intended use cases370- [ ] No broken links or missing resources