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.
When agents use itUse 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.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill react-native-development -a github-copilot
# or with the org installer (adds .github/skills/react-native-development):
npx -y github:AGCO-Global/org-skills add skill react-native-development
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-react-native-skills plugin, which bundles all Frontend / React Native skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-react-native-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill react-native-development -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill react-native-development -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill react-native-development -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill react-native-development -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
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
Which route file renders it? Expo Router file in app/ stays thin; the screen component lives in features/<f>/screens/.
What data, and does it work offline? Query hook with persisted cache; decide what shows with no network.
What platform behaviours differ? Back button (Android), swipe-back (iOS), keyboard, status bar, safe areas, haptics.
What permissions? Request lazily at the moment of use, with a pre-prompt explaining why.
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 ScrollViews 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 Stacks.
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.
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.
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.
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
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.
---
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`.