$npx -y skills add jezweb/claude-skills --skill react-nativeReact 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 nativ
| 1 | # React Native Patterns |
| 2 | |
| 3 | Performance and architecture patterns for React Native + Expo apps. Rules ranked by impact — fix CRITICAL before touching MEDIUM. |
| 4 | |
| 5 | This is a starting point. The skill will grow as you build more mobile apps. |
| 6 | |
| 7 | ## When to Apply |
| 8 | |
| 9 | - Building new React Native or Expo apps |
| 10 | - Optimising list and scroll performance |
| 11 | - Implementing animations |
| 12 | - Reviewing mobile code for performance issues |
| 13 | - Setting up a new Expo project |
| 14 | |
| 15 | ## 1. List Performance (CRITICAL) |
| 16 | |
| 17 | Lists are the #1 performance issue in React Native. A janky scroll kills the entire app experience. |
| 18 | |
| 19 | | Pattern | Problem | Fix | |
| 20 | |---------|---------|-----| |
| 21 | | **ScrollView for data** | `<ScrollView>` renders all items at once | Use `<FlatList>` or `<FlashList>` — virtualised, only renders visible items | |
| 22 | | **Missing keyExtractor** | FlatList without `keyExtractor` → unnecessary re-renders | `keyExtractor={(item) => item.id}` — stable unique key per item | |
| 23 | | **Complex renderItem** | Expensive component in renderItem re-renders on every scroll | Wrap in `React.memo`, extract to separate component | |
| 24 | | **Inline functions in renderItem** | `renderItem={({ item }) => <Row onPress={() => nav(item.id)} />}` | Extract handler: `const handlePress = useCallback(...)` | |
| 25 | | **No getItemLayout** | FlatList measures every item on scroll (expensive) | Provide `getItemLayout` for fixed-height items: `(data, index) => ({ length: 80, offset: 80 * index, index })` | |
| 26 | | **FlashList** | FlatList is good, FlashList is better for large lists | `@shopify/flash-list` — drop-in replacement, recycling architecture | |
| 27 | | **Large images in lists** | Full-res images decoded on main thread | Use `expo-image` with placeholder + transition, specify dimensions | |
| 28 | |
| 29 | ### FlatList Checklist |
| 30 | |
| 31 | Every FlatList should have: |
| 32 | ```tsx |
| 33 | <FlatList |
| 34 | data={items} |
| 35 | keyExtractor={(item) => item.id} |
| 36 | renderItem={renderItem} // Memoised component |
| 37 | getItemLayout={getItemLayout} // If items are fixed height |
| 38 | initialNumToRender={10} // Don't render 100 items on mount |
| 39 | maxToRenderPerBatch={10} // Batch size for off-screen rendering |
| 40 | windowSize={5} // How many screens to keep in memory |
| 41 | removeClippedSubviews={true} // Unmount off-screen items (Android) |
| 42 | /> |
| 43 | ``` |
| 44 | |
| 45 | ## 2. Animations (HIGH) |
| 46 | |
| 47 | Native animations run on the UI thread. JS animations block the JS thread and cause jank. |
| 48 | |
| 49 | | Pattern | Problem | Fix | |
| 50 | |---------|---------|-----| |
| 51 | | **Animated API for complex animations** | `Animated` runs on JS thread, blocks interactions | Use `react-native-reanimated` — runs on UI thread | |
| 52 | | **Layout animation** | Item appears/disappears with no transition | `LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)` | |
| 53 | | **Shared element transitions** | Navigate between screens, element teleports | `react-native-reanimated` shared transitions or `expo-router` shared elements | |
| 54 | | **Gesture + animation** | Drag/swipe feels laggy | `react-native-gesture-handler` + `reanimated` worklets — all on UI thread | |
| 55 | | **Measuring layout** | `onLayout` fires too late, causes flash | Use `useAnimatedStyle` with shared values for instant response | |
| 56 | |
| 57 | ### Reanimated Basics |
| 58 | |
| 59 | ```tsx |
| 60 | import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; |
| 61 | |
| 62 | function AnimatedBox() { |
| 63 | const offset = useSharedValue(0); |
| 64 | const style = useAnimatedStyle(() => ({ |
| 65 | transform: [{ translateX: withSpring(offset.value) }], |
| 66 | })); |
| 67 | |
| 68 | return ( |
| 69 | <GestureDetector gesture={panGesture}> |
| 70 | <Animated.View style={[styles.box, style]} /> |
| 71 | </GestureDetector> |
| 72 | ); |
| 73 | } |
| 74 | ``` |
| 75 | |
| 76 | ## 3. Navigation (HIGH) |
| 77 | |
| 78 | | Pattern | Problem | Fix | |
| 79 | |---------|---------|-----| |
| 80 | | **Expo Router** | File-based routing (like Next.js) for React Native | `app/` directory with `_layout.tsx` files. Preferred for new Expo projects. | |
| 81 | | **Heavy screens on stack** | Every screen stays mounted in the stack | Use `unmountOnBlur: true` for screens that don't need to persist | |
| 82 | | **Deep linking** | App doesn't respond to URLs | Expo Router handles this automatically. For bare RN: `Linking` API config | |
| 83 | | **Tab badge updates** | Badge count doesn't update when tab is focused | Use `useIsFocused()` or refetch on focus: `useFocusEffect(useCallback(...))` | |
| 84 | | **Navigation state persistence** | App loses position on background/kill | `onStateChange` + `initialState` with AsyncStorage | |
| 85 | |
| 86 | ### Expo Router Structure |
| 87 | |
| 88 | ``` |
| 89 | app/ |
| 90 | ├── _layout.tsx |