React Native Expert
Turns Claude into a senior React Native engineer who ships Expo-managed, New Architecture apps with TypeScript, tested navigation, 60fps lists, and a working EAS release pipeline.
When to Use This Skill
- Building screens, layouts, tabs, or modals with Expo Router file-based navigation
- Deciding between Expo managed workflow, prebuild (CNG), and bare React Native
- Migrating to or debugging the New Architecture (Fabric renderer, TurboModules, bridgeless mode)
- Fixing dropped frames in long lists (FlashList) or animations (Reanimated 3 worklets)
- Writing platform-specific code (iOS/Android/web) without forking the whole component tree
- Authoring native modules with the Expo Modules API (Swift/Kotlin) or consuming third-party ones
- Setting up EAS Build, EAS Update, and a Jest + React Native Testing Library + Maestro test stack
Core Workflow
- Analyze - read
package.json (RN and Expo SDK versions, newArchEnabled is default-on in 0.76+), app.json/app.config.ts (plugins, scheme, runtimeVersion), and the app/ directory to learn the route tree. Check for eas.json, metro.config.js, and an existing test setup before writing anything. Note whether the project is managed, prebuild, or bare - it changes what you are allowed to touch (ios//android/ are generated artifacts under prebuild).
- Implement - write TypeScript throughout. Use Expo Router conventions (
_layout.tsx, [param].tsx, (group)/) for navigation, typed routes via experiments.typedRoutes. Every interactive element gets accessibilityLabel/accessibilityRole and a stable testID - this is what makes the app automatable by Maestro and QA tooling, not optional polish.
- Verify types and lint - run
npx tsc --noEmit and npx expo lint; fix all reported issues and re-run until clean before proceeding.
- Verify Expo project health - run
npx expo-doctor after any dependency, config-plugin, or app.json change; fix all reported issues (usually with npx expo install --fix) and re-run until clean.
- Test - run
npx jest; write or update Jest + React Native Testing Library tests for the change (query by role/label/testID, never by implementation detail). If any test fails, fix the code or the test and re-run until all pass. For flows that cross screens, add or update a Maestro flow in .maestro/ and run maestro test .maestro/ until it passes.
- Prove it works on a device - run
npx expo start and exercise the change in a simulator/emulator or dev build (npx expo run:ios / run:android when native code changed). Confirm the actual behavior - navigation lands on the right screen, lists scroll at 60fps with the perf monitor open, both platforms if the change is platform-sensitive. If it misbehaves, fix and re-verify from step 3 until the observed behavior is correct.
Reference Guide
Load detailed guidance only when the task needs it:
| Topic |
Reference |
Load When |
| Expo Router navigation |
references/expo-router-navigation.md |
Creating/altering routes, layouts, tabs, modals, deep links, auth-gated navigation, typed routes |
| Lists & animations performance |
references/performance-lists-animations.md |
FlashList usage, dropped frames, Reanimated 3 worklets, gesture-driven UI, re-render storms |
| Native modules & New Architecture |
references/native-modules.md |
Writing an Expo Module (Swift/Kotlin), TurboModules/Fabric questions, config plugins, prebuild |
| Platform-specific code |
references/platform-handling.md |
iOS/Android/web divergence, safe areas, keyboard handling, permissions, platform file extensions |
| Testing & release |
references/testing-release.md |
Jest/RNTL setup, Maestro flows, accessibility/testID discipline, EAS Build/Update, versioning |
Key Patterns
Expo Router layout with auth gate (Router 5 / SDK 53+) - gate with <Stack.Protected> or a layout redirect; never conditionally render <Slot/> by hand:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { useSession } from '@/lib/session';
export default function RootLayout() {
const { user, isLoading } = useSession();
if (isLoading) return null; // keep splash until auth state known
return (
<Stack>
<Stack.Protected guard={!!user}>
<Stack.Screen name="(app)" />
</Stack.Protected>
<Stack.Protected guard={!user}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
</Stack>
);
}
FlashList, not FlatList, for long lists - fixed estimatedItemSize, memoized items, no inline arrow item renderers:
import { FlashList } from '@shopify/flash-list';
const renderItem = ({ item }: { item: Product }) => <ProductRow product={item} />;
<FlashList
data={products}
renderItem={renderItem}
estimatedItemSize={72}
keyExtractor={(p) => p.id}
testID="product-list"
/>
Reanimated 3 worklet - UI-thread animation reacting to shared values:
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
})); // runs on UI thread; never read React state here
return <Animated.Pressable style={style} => { offset.value += 40; }} />;
Accessible, automatable touch target - every interactive element carries label, role, and testID:
<Pressable
accessibilityRole="button"
accessibilityLabel="Add to cart"
testID="add-to-cart"
>
<Text>Add to cart</Text>
</Pressable>
Common Mistakes
- Reaching for
react-navigation config-first setup in an Expo Router app. SDK 53+ projects use file-based routing; adding a hand-rolled NavigationContainer breaks deep linking and typed routes. Create files under app/, use _layout.tsx for navigators, and router.push('/product/[id]') style navigation.
- Ejecting to bare workflow to add native code. Almost never needed since config plugins and the Expo Modules API. Prefer: config plugin → local Expo Module → prebuild (CNG). Bare is for apps that must own
ios//android/ (brownfield, exotic build systems). If ios/ exists under prebuild, treat it as generated - edit app.json plugins, not the Xcode project.
- Assuming the old bridge. RN 0.76+ defaults to New Architecture: no
requireNativeComponent bridging, NativeModules.X legacy modules may be missing, layout is synchronous under Fabric, and interop-layer warnings mean a dependency needs an upgrade - check library New Architecture support before adding it.
- Reading React state or calling JS functions inside worklets.
useAnimatedStyle/useAnimatedGestureHandler bodies run on the UI thread. Use shared values for data in, runOnJS for calls out; capturing component state gives stale values or crashes.
FlatList with anonymous renderItem and no memoization for feeds. Causes blank cells and dropped frames. Use FlashList with estimatedItemSize, hoist renderItem, memoize row components, and never nest a VirtualizedList inside a plain ScrollView.
- Shipping untestable UI. Missing
testID/accessibilityLabel makes Maestro and RNTL queries brittle (falling back to text matching that breaks with copy changes). Add both to every interactive element as you write it, not retroactively.
- Confusing EAS Update with app releases. OTA updates ship only JS/assets within the same
runtimeVersion; any native change (new library with native code, config plugin edit, SDK upgrade) requires a new EAS Build and store submission.
1---2name: react-native-expert3description: Use when building or debugging React Native apps - files using .tsx with react-native imports, app.json/app.config.ts, expo-router app/ directories, metro.config.js, eas.json, or mentions of Expo, Fabric, TurboModules, Reanimated, FlashList, or Maestro. Builds screens and navigation with Expo Router, integrates native modules, optimizes lists and animations, sets up EAS Build/Update, and writes Jest/RNTL and Maestro tests. Invoke for navigation and deep linking, New Architecture migration, list/animation performance, platform-specific code, native module authoring, and release pipeline setup.4license: MIT5---67# React Native Expert89Turns Claude into a senior React Native engineer who ships Expo-managed, New Architecture apps with TypeScript, tested navigation, 60fps lists, and a working EAS release pipeline.1011## When to Use This Skill1213- Building screens, layouts, tabs, or modals with Expo Router file-based navigation14- Deciding between Expo managed workflow, prebuild (CNG), and bare React Native15- Migrating to or debugging the New Architecture (Fabric renderer, TurboModules, bridgeless mode)16- Fixing dropped frames in long lists (FlashList) or animations (Reanimated 3 worklets)17- Writing platform-specific code (iOS/Android/web) without forking the whole component tree18- Authoring native modules with the Expo Modules API (Swift/Kotlin) or consuming third-party ones19- Setting up EAS Build, EAS Update, and a Jest + React Native Testing Library + Maestro test stack2021## Core Workflow22231. **Analyze** - read `package.json` (RN and Expo SDK versions, `newArchEnabled` is default-on in 0.76+), `app.json`/`app.config.ts` (plugins, scheme, runtimeVersion), and the `app/` directory to learn the route tree. Check for `eas.json`, `metro.config.js`, and an existing test setup before writing anything. Note whether the project is managed, prebuild, or bare - it changes what you are allowed to touch (`ios/`/`android/` are generated artifacts under prebuild).242. **Implement** - write TypeScript throughout. Use Expo Router conventions (`_layout.tsx`, `[param].tsx`, `(group)/`) for navigation, typed routes via `experiments.typedRoutes`. Every interactive element gets `accessibilityLabel`/`accessibilityRole` and a stable `testID` - this is what makes the app automatable by Maestro and QA tooling, not optional polish.253. **Verify types and lint** - run `npx tsc --noEmit` and `npx expo lint`; fix all reported issues and re-run until clean before proceeding.264. **Verify Expo project health** - run `npx expo-doctor` after any dependency, config-plugin, or `app.json` change; fix all reported issues (usually with `npx expo install --fix`) and re-run until clean.275. **Test** - run `npx jest`; write or update Jest + React Native Testing Library tests for the change (query by role/label/testID, never by implementation detail). If any test fails, fix the code or the test and re-run until all pass. For flows that cross screens, add or update a Maestro flow in `.maestro/` and run `maestro test .maestro/` until it passes.286. **Prove it works on a device** - run `npx expo start` and exercise the change in a simulator/emulator or dev build (`npx expo run:ios` / `run:android` when native code changed). Confirm the actual behavior - navigation lands on the right screen, lists scroll at 60fps with the perf monitor open, both platforms if the change is platform-sensitive. If it misbehaves, fix and re-verify from step 3 until the observed behavior is correct.2930## Reference Guide3132Load detailed guidance only when the task needs it:3334| Topic | Reference | Load When |35|-------|-----------|-----------|36| Expo Router navigation | `references/expo-router-navigation.md` | Creating/altering routes, layouts, tabs, modals, deep links, auth-gated navigation, typed routes |37| Lists & animations performance | `references/performance-lists-animations.md` | FlashList usage, dropped frames, Reanimated 3 worklets, gesture-driven UI, re-render storms |38| Native modules & New Architecture | `references/native-modules.md` | Writing an Expo Module (Swift/Kotlin), TurboModules/Fabric questions, config plugins, prebuild |39| Platform-specific code | `references/platform-handling.md` | iOS/Android/web divergence, safe areas, keyboard handling, permissions, platform file extensions |40| Testing & release | `references/testing-release.md` | Jest/RNTL setup, Maestro flows, accessibility/testID discipline, EAS Build/Update, versioning |4142## Key Patterns4344**Expo Router layout with auth gate (Router 5 / SDK 53+)** - gate with `<Stack.Protected>` or a layout redirect; never conditionally render `<Slot/>` by hand:4546```tsx47// app/_layout.tsx48import { Stack } from 'expo-router';49import { useSession } from '@/lib/session';5051export default function RootLayout() {52 const { user, isLoading } = useSession();53 if (isLoading) return null; // keep splash until auth state known54 return (55 <Stack>56 <Stack.Protected guard={!!user}>57 <Stack.Screen name="(app)" />58 </Stack.Protected>59 <Stack.Protected guard={!user}>60 <Stack.Screen name="sign-in" />61 </Stack.Protected>62 </Stack>63 );64}65```6667**FlashList, not FlatList, for long lists** - fixed `estimatedItemSize`, memoized items, no inline arrow item renderers:6869```tsx70import { FlashList } from '@shopify/flash-list';7172const renderItem = ({ item }: { item: Product }) => <ProductRow product={item} />;7374<FlashList75 data={products}76 renderItem={renderItem}77 estimatedItemSize={72}78 keyExtractor={(p) => p.id}79 testID="product-list"80/>81```8283**Reanimated 3 worklet - UI-thread animation reacting to shared values**:8485```tsx86import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';8788const offset = useSharedValue(0);89const style = useAnimatedStyle(() => ({90 transform: [{ translateX: withSpring(offset.value) }],91})); // runs on UI thread; never read React state here92return <Animated.Pressable style={style} onPress={() => { offset.value += 40; }} />;93```9495**Accessible, automatable touch target** - every interactive element carries label, role, and testID:9697```tsx98<Pressable99 accessibilityRole="button"100 accessibilityLabel="Add to cart"101 testID="add-to-cart"102 onPress={addToCart}103>104 <Text>Add to cart</Text>105</Pressable>106```107108## Common Mistakes109110- **Reaching for `react-navigation` config-first setup in an Expo Router app.** SDK 53+ projects use file-based routing; adding a hand-rolled `NavigationContainer` breaks deep linking and typed routes. Create files under `app/`, use `_layout.tsx` for navigators, and `router.push('/product/[id]')` style navigation.111- **Ejecting to bare workflow to add native code.** Almost never needed since config plugins and the Expo Modules API. Prefer: config plugin → local Expo Module → prebuild (CNG). Bare is for apps that must own `ios/`/`android/` (brownfield, exotic build systems). If `ios/` exists under prebuild, treat it as generated - edit `app.json` plugins, not the Xcode project.112- **Assuming the old bridge.** RN 0.76+ defaults to New Architecture: no `requireNativeComponent` bridging, `NativeModules.X` legacy modules may be missing, layout is synchronous under Fabric, and interop-layer warnings mean a dependency needs an upgrade - check library New Architecture support before adding it.113- **Reading React state or calling JS functions inside worklets.** `useAnimatedStyle`/`useAnimatedGestureHandler` bodies run on the UI thread. Use shared values for data in, `runOnJS` for calls out; capturing component state gives stale values or crashes.114- **`FlatList` with anonymous `renderItem` and no memoization for feeds.** Causes blank cells and dropped frames. Use FlashList with `estimatedItemSize`, hoist `renderItem`, memoize row components, and never nest a VirtualizedList inside a plain `ScrollView`.115- **Shipping untestable UI.** Missing `testID`/`accessibilityLabel` makes Maestro and RNTL queries brittle (falling back to text matching that breaks with copy changes). Add both to every interactive element as you write it, not retroactively.116- **Confusing EAS Update with app releases.** OTA updates ship only JS/assets within the same `runtimeVersion`; any native change (new library with native code, config plugin edit, SDK upgrade) requires a new EAS Build and store submission.