React Native
Purpose
Build React Native applications that feel native: lists that scroll at 60fps, navigation that matches platform conventions, and a release pipeline that does not surprise you at submission.
When to Use
- Building or reviewing a React Native or Expo application.
- Diagnosing list performance or dropped frames.
- Bridging to a native module.
- Setting up over-the-air updates and release builds.
Capabilities
- Navigation with React Navigation or Expo Router, including deep links.
- Platform-specific behavior and styling.
- List performance:
FlashList, virtualization, memoized rows.
- Native modules and the new architecture (Fabric, TurboModules).
- Offline support, secure storage, and background tasks.
- Release: EAS Build, OTA updates, store submission.
Inputs
- The feature set, target platforms, and minimum OS versions.
- Whether native modules are needed (this determines Expo Go versus a dev build).
- Performance requirements, especially for lists and animation.
Outputs
- A navigation structure with deep-link support.
- Lists that maintain frame rate with realistic data volumes.
- A build and release pipeline for both platforms.
Workflow
- Choose the runtime honestly — Expo with a development build gives you native modules and the Expo tooling. Expo Go is for prototypes; bare React Native is for teams that need full native control and are prepared to maintain it.
- Structure navigation first — Stacks inside tabs, with a typed route map. Retrofitting deep links into an untyped navigator is painful.
- Fix lists before they are slow —
FlashList with a stable keyExtractor, memoized row components, and a fixed item size where possible. ScrollView with a hundred children will drop frames on a mid-range Android.
- Handle platform differences explicitly —
Platform.select for behavior, not just style. Back-button behavior, safe areas, and permission flows differ genuinely.
- Move animation off the JS thread — Reanimated worklets run on the UI thread. Animating via
setState will jank the moment the JS thread does anything else.
- Test the release build — Debug builds hide performance problems entirely. Profile in release, on a real mid-range device.
Best Practices
- The JS thread and the UI thread are separate. Any heavy JavaScript blocks interaction, and it will not show up in a simulator on a fast laptop.
- An inline arrow function as a
renderItem re-creates every row on every render. Memoize the row component and hoist the callback.
useNativeDriver: true on every Animated usage, or use Reanimated. Without it, every frame crosses the bridge.
- Store tokens in
expo-secure-store or Keychain — never AsyncStorage, which is unencrypted plain text.
- Images are the main cause of memory pressure. Size them to the display size; do not load a 4000px original into a 100px thumbnail.
- OTA updates cannot change native code. If you added a native module, the update requires a store release.
Examples
A list that stays at 60fps:
const OrderRow = memo(function OrderRow({ order, onPress }: OrderRowProps) {
return (
<Pressable => onPress(order.id)} style={styles.row}>
<Text style={styles.title}>{order.reference}</Text>
<Text style={styles.meta}>{order.customerName}</Text>
</Pressable>
);
});
export function OrderList({ orders }: { orders: Order[] }) {
const router = useRouter();
const string) => router.push(`/orders/${id}`), [router]);
return (
<FlashList
data={orders}
renderItem={({ item }) => <OrderRow order={item} />}
keyExtractor={(item) => item.id}
estimatedItemSize={72} // FlashList needs this to recycle correctly
removeClippedSubviews
/>
);
}
onPress is hoisted and stable, OrderRow is memoized, so scrolling re-renders nothing that has not changed.
Animation on the UI thread:
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: withSpring(offset.value) }],
}));
// Runs on the UI thread — unaffected by whatever the JS thread is doing.
Notes
- The new architecture (Fabric + TurboModules) removes the asynchronous bridge and is the default from React Native 0.76. Older third-party native modules may not be compatible — check before upgrading.
estimatedItemSize on FlashList is not optional in practice; a bad estimate causes visible blank space during fast scrolls.
- Hermes is the default engine and materially improves startup time and memory. If a project is still on JSC, that is usually an oversight.
1---2name: react-native3description: Use when building cross-platform mobile apps with React Native or Expo. Covers navigation, platform differences, list performance, native modules, offline behavior, and release builds.4---56# React Native78## Purpose910Build React Native applications that feel native: lists that scroll at 60fps, navigation that matches platform conventions, and a release pipeline that does not surprise you at submission.1112## When to Use1314- Building or reviewing a React Native or Expo application.15- Diagnosing list performance or dropped frames.16- Bridging to a native module.17- Setting up over-the-air updates and release builds.1819## Capabilities2021- Navigation with React Navigation or Expo Router, including deep links.22- Platform-specific behavior and styling.23- List performance: `FlashList`, virtualization, memoized rows.24- Native modules and the new architecture (Fabric, TurboModules).25- Offline support, secure storage, and background tasks.26- Release: EAS Build, OTA updates, store submission.2728## Inputs2930- The feature set, target platforms, and minimum OS versions.31- Whether native modules are needed (this determines Expo Go versus a dev build).32- Performance requirements, especially for lists and animation.3334## Outputs3536- A navigation structure with deep-link support.37- Lists that maintain frame rate with realistic data volumes.38- A build and release pipeline for both platforms.3940## Workflow41421. **Choose the runtime honestly** — Expo with a development build gives you native modules and the Expo tooling. Expo Go is for prototypes; bare React Native is for teams that need full native control and are prepared to maintain it.432. **Structure navigation first** — Stacks inside tabs, with a typed route map. Retrofitting deep links into an untyped navigator is painful.443. **Fix lists before they are slow** — `FlashList` with a stable `keyExtractor`, memoized row components, and a fixed item size where possible. `ScrollView` with a hundred children will drop frames on a mid-range Android.454. **Handle platform differences explicitly** — `Platform.select` for behavior, not just style. Back-button behavior, safe areas, and permission flows differ genuinely.465. **Move animation off the JS thread** — Reanimated worklets run on the UI thread. Animating via `setState` will jank the moment the JS thread does anything else.476. **Test the release build** — Debug builds hide performance problems entirely. Profile in release, on a real mid-range device.4849## Best Practices5051- The JS thread and the UI thread are separate. Any heavy JavaScript blocks interaction, and it will not show up in a simulator on a fast laptop.52- An inline arrow function as a `renderItem` re-creates every row on every render. Memoize the row component and hoist the callback.53- `useNativeDriver: true` on every `Animated` usage, or use Reanimated. Without it, every frame crosses the bridge.54- Store tokens in `expo-secure-store` or Keychain — never `AsyncStorage`, which is unencrypted plain text.55- Images are the main cause of memory pressure. Size them to the display size; do not load a 4000px original into a 100px thumbnail.56- OTA updates cannot change native code. If you added a native module, the update requires a store release.5758## Examples5960**A list that stays at 60fps:**6162```tsx63const OrderRow = memo(function OrderRow({ order, onPress }: OrderRowProps) {64 return (65 <Pressable onPress={() => onPress(order.id)} style={styles.row}>66 <Text style={styles.title}>{order.reference}</Text>67 <Text style={styles.meta}>{order.customerName}</Text>68 </Pressable>69 );70});7172export function OrderList({ orders }: { orders: Order[] }) {73 const router = useRouter();74 const onPress = useCallback((id: string) => router.push(`/orders/${id}`), [router]);7576 return (77 <FlashList78 data={orders}79 renderItem={({ item }) => <OrderRow order={item} onPress={onPress} />}80 keyExtractor={(item) => item.id}81 estimatedItemSize={72} // FlashList needs this to recycle correctly82 removeClippedSubviews83 />84 );85}86```8788`onPress` is hoisted and stable, `OrderRow` is memoized, so scrolling re-renders nothing that has not changed.8990**Animation on the UI thread:**9192```tsx93const offset = useSharedValue(0);94const style = useAnimatedStyle(() => ({95 transform: [{ translateY: withSpring(offset.value) }],96}));9798// Runs on the UI thread — unaffected by whatever the JS thread is doing.99```100101## Notes102103- The new architecture (Fabric + TurboModules) removes the asynchronous bridge and is the default from React Native 0.76. Older third-party native modules may not be compatible — check before upgrading.104- `estimatedItemSize` on `FlashList` is not optional in practice; a bad estimate causes visible blank space during fast scrolls.105- Hermes is the default engine and materially improves startup time and memory. If a project is still on JSC, that is usually an oversight.