React Native Patterns
Performance and architecture patterns for React Native + Expo apps. Rules ranked by impact — fix CRITICAL before touching MEDIUM.
This is a starting point. The skill will grow as you build more mobile apps.
When to Apply
- Building new React Native or Expo apps
- Optimising list and scroll performance
- Implementing animations
- Reviewing mobile code for performance issues
- Setting up a new Expo project
1. List Performance (CRITICAL)
Lists are the #1 performance issue in React Native. A janky scroll kills the entire app experience.
| Pattern |
Problem |
Fix |
| ScrollView for data |
<ScrollView> renders all items at once |
Use <FlatList> or <FlashList> — virtualised, only renders visible items |
| Missing keyExtractor |
FlatList without keyExtractor → unnecessary re-renders |
keyExtractor={(item) => item.id} — stable unique key per item |
| Complex renderItem |
Expensive component in renderItem re-renders on every scroll |
Wrap in React.memo, extract to separate component |
| Inline functions in renderItem |
renderItem={({ item }) => <Row => nav(item.id)} />} |
Extract handler: const handlePress = useCallback(...) |
| No getItemLayout |
FlatList measures every item on scroll (expensive) |
Provide getItemLayout for fixed-height items: (data, index) => ({ length: 80, offset: 80 * index, index }) |
| FlashList |
FlatList is good, FlashList is better for large lists |
@shopify/flash-list — drop-in replacement, recycling architecture |
| Large images in lists |
Full-res images decoded on main thread |
Use expo-image with placeholder + transition, specify dimensions |
FlatList Checklist
Every FlatList should have:
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // Memoised component
getItemLayout={getItemLayout} // If items are fixed height
initialNumToRender={10} // Don't render 100 items on mount
maxToRenderPerBatch={10} // Batch size for off-screen rendering
windowSize={5} // How many screens to keep in memory
removeClippedSubviews={true} // Unmount off-screen items (Android)
/>
2. Animations (HIGH)
Native animations run on the UI thread. JS animations block the JS thread and cause jank.
| Pattern |
Problem |
Fix |
| Animated API for complex animations |
Animated runs on JS thread, blocks interactions |
Use react-native-reanimated — runs on UI thread |
| Layout animation |
Item appears/disappears with no transition |
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) |
| Shared element transitions |
Navigate between screens, element teleports |
react-native-reanimated shared transitions or expo-router shared elements |
| Gesture + animation |
Drag/swipe feels laggy |
react-native-gesture-handler + reanimated worklets — all on UI thread |
| Measuring layout |
onLayout fires too late, causes flash |
Use useAnimatedStyle with shared values for instant response |
Reanimated Basics
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, style]} />
</GestureDetector>
);
}
3. Navigation (HIGH)
| Pattern |
Problem |
Fix |
| Expo Router |
File-based routing (like Next.js) for React Native |
app/ directory with _layout.tsx files. Preferred for new Expo projects. |
| Heavy screens on stack |
Every screen stays mounted in the stack |
Use unmountOnBlur: true for screens that don't need to persist |
| Deep linking |
App doesn't respond to URLs |
Expo Router handles this automatically. For bare RN: Linking API config |
| Tab badge updates |
Badge count doesn't update when tab is focused |
Use useIsFocused() or refetch on focus: useFocusEffect(useCallback(...)) |
| Navigation state persistence |
App loses position on background/kill |
onStateChange + initialState with AsyncStorage |
Expo Router Structure
app/
├── _layout.tsx # Root layout (tab navigator)
├── index.tsx # Home tab
├── (tabs)/
│ ├── _layout.tsx # Tab bar config
│ ├── home.tsx
│ ├── search.tsx
│ └── profile.tsx
├── [id].tsx # Dynamic route
└── modal.tsx # Modal route
4. UI Patterns (HIGH)
| Pattern |
Problem |
Fix |
| Safe area |
Content under notch or home indicator |
<SafeAreaView> or useSafeAreaInsets() from react-native-safe-area-context |
| Keyboard avoidance |
Form fields hidden behind keyboard |
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> |
| Platform-specific code |
iOS and Android need different behaviour |
Platform.select({ ios: ..., android: ... }) or .ios.tsx / .android.tsx files |
| Status bar |
Status bar overlaps content or wrong colour |
<StatusBar style="auto" /> from expo-status-bar in root layout |
| Touch targets |
Buttons too small to tap |
Minimum 44x44pt. Use hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} |
| Haptic feedback |
Taps feel dead |
expo-haptics — Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) on important actions |
5. Images and Media (MEDIUM)
| Pattern |
Problem |
Fix |
| Image component |
<Image> from react-native is basic |
Use expo-image — caching, placeholder, transition, blurhash |
| Remote images without dimensions |
Layout shift when image loads |
Always specify width and height, or use aspectRatio |
| Large images |
OOM crashes on Android |
Resize server-side or use expo-image which handles memory |
| SVG |
SVG support isn't native |
react-native-svg + react-native-svg-transformer for SVG imports |
| Video |
Video playback |
expo-av or expo-video (newer API) |
6. State and Data (MEDIUM)
| Pattern |
Problem |
Fix |
| AsyncStorage for complex data |
JSON parse/stringify on every read |
Use MMKV (react-native-mmkv) — 30x faster than AsyncStorage |
| Global state |
Redux/MobX boilerplate for simple state |
Zustand — minimal, works great with React Native |
| Server state |
Manual fetch + loading + error + cache |
TanStack Query — same as web, works in React Native |
| Offline first |
App unusable without network |
TanStack Query persistQueryClient + MMKV, or WatermelonDB for complex offline |
| Deep state updates |
Spread operator hell for nested objects |
Immer via Zustand: set(produce(state => { state.user.name = 'new' })) |
7. Expo Workflow (MEDIUM)
| Pattern |
When |
How |
| Development build |
Need native modules |
npx expo run:ios or eas build --profile development |
| Expo Go |
Quick prototyping, no native modules |
npx expo start — scan QR code |
| EAS Build |
CI/CD, app store builds |
eas build --platform ios --profile production |
| EAS Update |
Hot fix without app store review |
eas update --branch production --message "Fix bug" |
| Config plugins |
Modify native config without ejecting |
app.config.ts with expo-build-properties or custom config plugin |
| Environment variables |
Different configs per build |
eas.json build profiles + expo-constants |
New Project Setup
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context
8. Testing (LOW-MEDIUM)
| Tool |
For |
Setup |
| Jest |
Unit tests, hook tests |
Included with Expo by default |
| React Native Testing Library |
Component tests |
@testing-library/react-native |
| Detox |
E2E tests on real devices/simulators |
detox — Wix's testing framework |
| Maestro |
E2E with YAML flows |
maestro test flow.yaml — simpler than Detox |
Common Gotchas
| Gotcha |
Fix |
| Metro bundler cache |
npx expo start --clear |
| Pod install issues (iOS) |
cd ios && pod install --repo-update |
| Reanimated not working |
Must be first import: import 'react-native-reanimated' in root |
| Expo SDK upgrade |
npx expo install --fix after updating SDK version |
| Android build fails |
Check gradle.properties for memory: org.gradle.jvmargs=-Xmx4g |
| iOS simulator slow |
Use physical device for performance testing — simulator doesn't reflect real perf |
1---2name: react-native3description: React Native and Expo patterns for building performant mobile apps. Covers list performance, animations with Reanimated, navigation, UI patterns, state management, platform-specific code, and Expo workflows. Use when building or reviewing React Native code. Triggers: 'react native', 'expo', 'mobile app', 'react native performance', 'flatlist', 'reanimated', 'expo router', 'mobile development', 'ios app', 'android app'.4---56# React Native Patterns78Performance and architecture patterns for React Native + Expo apps. Rules ranked by impact — fix CRITICAL before touching MEDIUM.910This is a starting point. The skill will grow as you build more mobile apps.1112## When to Apply1314- Building new React Native or Expo apps15- Optimising list and scroll performance16- Implementing animations17- Reviewing mobile code for performance issues18- Setting up a new Expo project1920## 1. List Performance (CRITICAL)2122Lists are the #1 performance issue in React Native. A janky scroll kills the entire app experience.2324| Pattern | Problem | Fix |25|---------|---------|-----|26| **ScrollView for data** | `<ScrollView>` renders all items at once | Use `<FlatList>` or `<FlashList>` — virtualised, only renders visible items |27| **Missing keyExtractor** | FlatList without `keyExtractor` → unnecessary re-renders | `keyExtractor={(item) => item.id}` — stable unique key per item |28| **Complex renderItem** | Expensive component in renderItem re-renders on every scroll | Wrap in `React.memo`, extract to separate component |29| **Inline functions in renderItem** | `renderItem={({ item }) => <Row onPress={() => nav(item.id)} />}` | Extract handler: `const handlePress = useCallback(...)` |30| **No getItemLayout** | FlatList measures every item on scroll (expensive) | Provide `getItemLayout` for fixed-height items: `(data, index) => ({ length: 80, offset: 80 * index, index })` |31| **FlashList** | FlatList is good, FlashList is better for large lists | `@shopify/flash-list` — drop-in replacement, recycling architecture |32| **Large images in lists** | Full-res images decoded on main thread | Use `expo-image` with placeholder + transition, specify dimensions |3334### FlatList Checklist3536Every FlatList should have:37```tsx38<FlatList39 data={items}40 keyExtractor={(item) => item.id}41 renderItem={renderItem} // Memoised component42 getItemLayout={getItemLayout} // If items are fixed height43 initialNumToRender={10} // Don't render 100 items on mount44 maxToRenderPerBatch={10} // Batch size for off-screen rendering45 windowSize={5} // How many screens to keep in memory46 removeClippedSubviews={true} // Unmount off-screen items (Android)47/>48```4950## 2. Animations (HIGH)5152Native animations run on the UI thread. JS animations block the JS thread and cause jank.5354| Pattern | Problem | Fix |55|---------|---------|-----|56| **Animated API for complex animations** | `Animated` runs on JS thread, blocks interactions | Use `react-native-reanimated` — runs on UI thread |57| **Layout animation** | Item appears/disappears with no transition | `LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)` |58| **Shared element transitions** | Navigate between screens, element teleports | `react-native-reanimated` shared transitions or `expo-router` shared elements |59| **Gesture + animation** | Drag/swipe feels laggy | `react-native-gesture-handler` + `reanimated` worklets — all on UI thread |60| **Measuring layout** | `onLayout` fires too late, causes flash | Use `useAnimatedStyle` with shared values for instant response |6162### Reanimated Basics6364```tsx65import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';6667function AnimatedBox() {68 const offset = useSharedValue(0);69 const style = useAnimatedStyle(() => ({70 transform: [{ translateX: withSpring(offset.value) }],71 }));7273 return (74 <GestureDetector gesture={panGesture}>75 <Animated.View style={[styles.box, style]} />76 </GestureDetector>77 );78}79```8081## 3. Navigation (HIGH)8283| Pattern | Problem | Fix |84|---------|---------|-----|85| **Expo Router** | File-based routing (like Next.js) for React Native | `app/` directory with `_layout.tsx` files. Preferred for new Expo projects. |86| **Heavy screens on stack** | Every screen stays mounted in the stack | Use `unmountOnBlur: true` for screens that don't need to persist |87| **Deep linking** | App doesn't respond to URLs | Expo Router handles this automatically. For bare RN: `Linking` API config |88| **Tab badge updates** | Badge count doesn't update when tab is focused | Use `useIsFocused()` or refetch on focus: `useFocusEffect(useCallback(...))` |89| **Navigation state persistence** | App loses position on background/kill | `onStateChange` + `initialState` with AsyncStorage |9091### Expo Router Structure9293```94app/95├── _layout.tsx # Root layout (tab navigator)96├── index.tsx # Home tab97├── (tabs)/98│ ├── _layout.tsx # Tab bar config99│ ├── home.tsx100│ ├── search.tsx101│ └── profile.tsx102├── [id].tsx # Dynamic route103└── modal.tsx # Modal route104```105106## 4. UI Patterns (HIGH)107108| Pattern | Problem | Fix |109|---------|---------|-----|110| **Safe area** | Content under notch or home indicator | `<SafeAreaView>` or `useSafeAreaInsets()` from `react-native-safe-area-context` |111| **Keyboard avoidance** | Form fields hidden behind keyboard | `<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>` |112| **Platform-specific code** | iOS and Android need different behaviour | `Platform.select({ ios: ..., android: ... })` or `.ios.tsx` / `.android.tsx` files |113| **Status bar** | Status bar overlaps content or wrong colour | `<StatusBar style="auto" />` from `expo-status-bar` in root layout |114| **Touch targets** | Buttons too small to tap | Minimum 44x44pt. Use `hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}` |115| **Haptic feedback** | Taps feel dead | `expo-haptics` — `Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)` on important actions |116117## 5. Images and Media (MEDIUM)118119| Pattern | Problem | Fix |120|---------|---------|-----|121| **Image component** | `<Image>` from react-native is basic | Use `expo-image` — caching, placeholder, transition, blurhash |122| **Remote images without dimensions** | Layout shift when image loads | Always specify `width` and `height`, or use `aspectRatio` |123| **Large images** | OOM crashes on Android | Resize server-side or use `expo-image` which handles memory |124| **SVG** | SVG support isn't native | `react-native-svg` + `react-native-svg-transformer` for SVG imports |125| **Video** | Video playback | `expo-av` or `expo-video` (newer API) |126127## 6. State and Data (MEDIUM)128129| Pattern | Problem | Fix |130|---------|---------|-----|131| **AsyncStorage for complex data** | JSON parse/stringify on every read | Use MMKV (`react-native-mmkv`) — 30x faster than AsyncStorage |132| **Global state** | Redux/MobX boilerplate for simple state | Zustand — minimal, works great with React Native |133| **Server state** | Manual fetch + loading + error + cache | TanStack Query — same as web, works in React Native |134| **Offline first** | App unusable without network | TanStack Query `persistQueryClient` + MMKV, or WatermelonDB for complex offline |135| **Deep state updates** | Spread operator hell for nested objects | Immer via Zustand: `set(produce(state => { state.user.name = 'new' }))` |136137## 7. Expo Workflow (MEDIUM)138139| Pattern | When | How |140|---------|------|-----|141| **Development build** | Need native modules | `npx expo run:ios` or `eas build --profile development` |142| **Expo Go** | Quick prototyping, no native modules | `npx expo start` — scan QR code |143| **EAS Build** | CI/CD, app store builds | `eas build --platform ios --profile production` |144| **EAS Update** | Hot fix without app store review | `eas update --branch production --message "Fix bug"` |145| **Config plugins** | Modify native config without ejecting | `app.config.ts` with `expo-build-properties` or custom config plugin |146| **Environment variables** | Different configs per build | `eas.json` build profiles + `expo-constants` |147148### New Project Setup149150```bash151npx create-expo-app my-app --template tabs152cd my-app153npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context154```155156## 8. Testing (LOW-MEDIUM)157158| Tool | For | Setup |159|------|-----|-------|160| **Jest** | Unit tests, hook tests | Included with Expo by default |161| **React Native Testing Library** | Component tests | `@testing-library/react-native` |162| **Detox** | E2E tests on real devices/simulators | `detox` — Wix's testing framework |163| **Maestro** | E2E with YAML flows | `maestro test flow.yaml` — simpler than Detox |164165## Common Gotchas166167| Gotcha | Fix |168|--------|-----|169| Metro bundler cache | `npx expo start --clear` |170| Pod install issues (iOS) | `cd ios && pod install --repo-update` |171| Reanimated not working | Must be first import: `import 'react-native-reanimated'` in root |172| Expo SDK upgrade | `npx expo install --fix` after updating SDK version |173| Android build fails | Check `gradle.properties` for memory: `org.gradle.jvmargs=-Xmx4g` |174| iOS simulator slow | Use physical device for performance testing — simulator doesn't reflect real perf |