---
name: react-native-development
description: Write production-quality React Native / Expo code day to day — screens, components, styling with StyleSheet/NativeWind and theme tokens, safe areas and keyboard handling, lists, navigation with Expo Router, forms, images, permissions and Expo SDK modules, platform differences, accessibility, offline and error states, and gestures/animations with Reanimated. Use this whenever the user asks to build, implement, refactor or fix a React Native or Expo screen, component, hook, or feature; asks "how do I do X in React Native"; pastes RN code with a bug; or asks about ScrollView vs FlatList, KeyboardAvoidingView, SafeAreaView, Pressable, Platform.select, expo-image, expo-router, permissions, or deep links. Also use it when generating any new React Native code, even small snippets.
metadata:
  technology: React Native
  type: development
---

# React Native Development

Web React habits mostly transfer; the places they don't (no CSS cascade, touch not click, two threads, native permissions, app-store rules) are where bugs live. This skill is the day-to-day implementation guide; architecture and performance have their own skills.

## 1. Before writing a screen

1. **Which route file renders it?** Expo Router file in `app/` stays thin; the screen component lives in `features/<f>/screens/`.
2. **What data, and does it work offline?** Query hook with persisted cache; decide what shows with no network.
3. **What platform behaviours differ?** Back button (Android), swipe-back (iOS), keyboard, status bar, safe areas, haptics.
4. **What permissions?** Request lazily at the moment of use, with a pre-prompt explaining why.

## 2. Screen template

```tsx
export function OrderDetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const { data, isPending, isError, refetch } = useOrder(id);
  const insets = useSafeAreaInsets();

  if (isPending) return <ScreenSkeleton />;
  if (isError) return <ErrorState onRetry={refetch} />;

  return (
    <View style={[styles.root, { paddingBottom: insets.bottom }]}>
      <Stack.Screen options={{ title: data.number }} />
      <ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
        <OrderSummary order={data} />
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: tokens.color.bg },
  content: { padding: tokens.space[4], gap: tokens.space[3] },
});
```

Conventions:
- **`StyleSheet.create` at module bottom**, styles named by role (`root`, `content`, `title`), tokens from `shared/theme`; no magic numbers.
- **`flex: 1` on the root**, `gap` for spacing between children (supported) instead of margin hacks.
- **Safe areas via `useSafeAreaInsets()`** (react-native-safe-area-context) applied as padding — not `SafeAreaView` inside tabs/stacks, which double-pads.
- **Header/title configured with `<Stack.Screen options>`** in the screen, not the layout, when it depends on data.
- Typed params: `useLocalSearchParams<{ id: string }>()`; validate/parse (`Number(id)`) before use.

## 3. Components and touch

- **`Pressable`** for anything tappable (never `TouchableOpacity` in new code): `android_ripple`, `style={({ pressed }) => …}`, `hitSlop` to reach 44 pt, `accessibilityRole="button"`, `accessibilityLabel`, `accessibilityState={{ disabled }}`.
- **`Text` must wrap all strings** — a bare string inside `View` crashes. Set `allowFontScaling` policy and `maxFontSizeMultiplier` on constrained labels; test at 200 % dynamic type.
- **No nested `ScrollView`s in the same direction**; lists inside a screen are `FlashList` with `ListHeaderComponent`, not `ScrollView` + `map`.
- **Platform differences**: `Platform.select({ ios, android })` inline for small values; `Component.ios.tsx` / `.android.tsx` for divergent implementations; `Platform.OS === 'web'` when targeting web too.
- Shadows: `shadow*` props on iOS, `elevation` on Android; wrap in a helper (`shadow(2)`) so you never write both by hand.
- Borders need explicit `borderWidth`; `overflow: 'hidden'` clips `borderRadius` on Android for images/children.
- Fonts loaded once with `useFonts` behind the splash; reference by `fontFamily` string from tokens.

## 4. Lists

- **FlashList**: `data`, `renderItem`, `keyExtractor`, `estimatedItemSize`, `ItemSeparatorComponent`, `ListEmptyComponent`, `onEndReached` + `onEndReachedThreshold={0.5}` for infinite scroll, `refreshing`/`onRefresh` for pull-to-refresh.
- Row components: memoised, receive primitives or stable objects, no inline closures capturing the row — pass `id` and let the row call the callback.
- `getItemType` when rows differ in layout (headers vs items).
- Sticky headers via `stickyHeaderIndices`; sections via `SectionList` or FlashList with typed items.

## 5. Navigation (Expo Router)

- **Push vs replace vs navigate**: `router.push` adds to stack, `router.replace` swaps (after login/complete), `router.navigate` goes to existing if present.
- `<Link href="/orders/[id]" asChild><Pressable>…</Pressable></Link>` for tappable navigation with prefetch; `href` typed with typed routes.
- Modals: route group `(modals)` with `presentation: 'modal'`; dismiss via `router.back()` / `router.dismiss()`.
- Auth gating in the root layout via `<Redirect>` based on session; never conditionally render different `Stack`s.
- Deep links: every screen reachable by URL; parse and validate params; handle missing entity with a designed 404 screen.
- Android back: `useFocusEffect` + `BackHandler` only for genuine intercepts (unsaved changes); otherwise let the router handle it.
- Tabs: `<Tabs.Screen options={{ tabBarIcon, title, href: null /* hide */ }}>`; badge via `tabBarBadge`.

## 6. Keyboard and forms

- `KeyboardAvoidingView` with `behavior={Platform.OS === 'ios' ? 'padding' : undefined}` at the screen level, or **react-native-keyboard-controller** for reliable cross-platform behaviour.
- `ScrollView keyboardShouldPersistTaps="handled"` so taps on buttons work while the keyboard is up; `keyboardDismissMode="on-drag"`.
- Inputs: `returnKeyType`, `onSubmitEditing` to focus the next field (`ref.current?.focus()`), `autoComplete`/`textContentType` for autofill, `keyboardType`, `autoCapitalize="none"` for emails.
- **React Hook Form + Zod** with `Controller` (RN inputs are controlled); errors shown under the field with `accessibilityLiveRegion="polite"`.
- Disable submit while pending; dismiss keyboard (`Keyboard.dismiss()`) before showing success.

## 7. Images and assets

- **`expo-image`** everywhere: `source={{ uri }}`, `placeholder={blurhash}`, `contentFit="cover"`, `transition={200}`, `cachePolicy`, explicit `style` dimensions.
- Local assets via `require('@/assets/x.png')` centralised in `assets/index.ts`; use `@2x`/`@3x` variants or SVG via `react-native-svg`.
- Icons: `@expo/vector-icons` or a custom SVG icon component with `accessibilityLabel` when meaningful.

## 8. Device APIs, permissions, native modules

- Use the Expo SDK module (`expo-camera`, `expo-location`, `expo-notifications`, `expo-haptics`, `expo-clipboard`, `expo-secure-store`, `expo-file-system`) before any community lib.
- Permissions: check status → show your own explanation → request → handle `denied`/`blocked` with a "Open settings" (`Linking.openSettings()`) path. Add `NSxxxUsageDescription` / Android permissions via config plugin in `app.json`, never by editing native folders.
- Wrap module calls in `try/catch`; simulators lack camera/biometrics — guard with `isAvailableAsync()`.
- Haptics on meaningful confirmations only; respect system reduce-motion via `AccessibilityInfo.isReduceMotionEnabled`.

## 9. Animations and gestures (implementation)

- **Reanimated**: `useSharedValue`, `useAnimatedStyle`, `withTiming`/`withSpring`; entering/exiting layout animations (`FadeIn`, `Layout`); never `Animated` from core in new code.
- **Gesture Handler**: `Gesture.Pan()/Tap()/Pinch()` + `GestureDetector`; `runOnJS` for callbacks into React; wrap app root in `GestureHandlerRootView`.
- Bottom sheets: `@gorhom/bottom-sheet`; don't hand-roll.
- Keep animation logic in a hook (`useSwipeToDismiss`) so components stay declarative.

## 10. Storage, network, offline (implementation)

- Query hooks per feature; show cached data instantly with a subtle "updated x ago"/offline banner from `useNetInfo`.
- MMKV for settings/cache flags (`storage.getString`), SecureStore for tokens (`await SecureStore.getItemAsync`).
- Mutations while offline: queue via TanStack Query's mutation cache with `networkMode: 'offlineFirst'`, or show a clear "will sync when online" state — never silently fail.
- All fetches through the shared `apiClient` (base URL, auth header refresh, timeout via `AbortController`).

## 11. Accessibility defaults

- Every interactive element: `accessibilityRole`, `accessibilityLabel` (when text isn't the label), `accessibilityState`, `accessibilityHint` for non-obvious results.
- Group related text with `accessible` on the container so the screen reader reads once.
- Announce results: `AccessibilityInfo.announceForAccessibility()` after async actions.
- Test with VoiceOver/TalkBack for one key flow per feature; check dynamic type at max.

## 12. Definition of done for a screen

- [ ] Loading, empty, error (with retry), offline states rendered
- [ ] Safe areas and keyboard verified on iOS notch device and small Android
- [ ] Typed route params, deep-linkable, back behaviour correct on Android
- [ ] Touch targets ≥ 44 pt, labels set, screen reader pass
- [ ] Tokens only — no hardcoded colours/sizes; dark mode checked
- [ ] Jest test for logic + snapshot-free component test (see testing skill); works in release build

## Anti-patterns to fix on sight

- `ScrollView` + `.map()` for data lists; nested same-direction scroll views.
- `TouchableOpacity`/`TouchableHighlight` in new code; `<View onTouchEnd>` as a button.
- Strings outside `<Text>`; hardcoded font sizes without scaling policy.
- `Dimensions.get('window')` at module scope; `SafeAreaView` inside nested navigators.
- `AsyncStorage` for tokens; tokens in Zustand persisted to MMKV.
- Editing `ios/`/`android/` folders in a managed Expo app instead of a config plugin.
- Requesting all permissions at launch.
- `Animated.Value` + `setState` per frame; `PanResponder`.
- Business logic in `app/` route files or `_layout.tsx`.
