React Native with Expo skill for Claude Code
Dev builds, Expo Router and OTA updates that are safe. Claude works in Expo projects the way the toolchain expects: native libraries via npx expo install, native settings through app config and plugins instead of hand-edited ios and android folders, development builds once custom native code appears, and OTA updates only to binaries with a matching runtime version. It checks safe areas, keyboard, back handling and permissions on both platforms.
- Name
react-native-expo- Size
- 223 lines · ≈2,400 tokens
- Files
- 1
- License
- MIT
- Updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and ships React Native apps with Expo — SDK upgrades, Expo Router file-based navigation, development builds vs. Expo Go, Continuous Native Generation with config plugins, EAS Build, Submit, and Update with runtime versions and channels, Expo Modules for native code, and iOS/Android platform differences (safe areas, keyboard, edge-to-edge, permissions). Use when creating or maintaining an Expo app, adding a native library, configuring app.json or eas.json, shipping an OTA update, or fixing something that works on one platform but not the other. Try asking
- Set up Expo Router with tabs and a protected sign-in flow
- Ship this checkout fix as an OTA update without breaking older app versions
- This screen's keyboard covers the input on Android but not on iOS
Use it when
- Creating or upgrading an Expo app or SDK version
- Adding a native library, permission or config plugin
- Configuring eas.json, channels and runtime versions
- Fixing behavior that differs between iOS and Android
Skip it when
- You are preparing store listings, signing and phased rollout — use app-store-release
- The app is fully native Swift or Kotlin — use swiftui-views or jetpack-compose
How to check the result
expo-doctor is clean, the change works in a development or preview build on a physical iPhone and Android phone, and OTA updates apply after two cold starts on a matching build.
The common mistake it prevents
Publishing an OTA update after adding a native dependency, which crashes installed binaries that do not contain that native code.
A healthy Expo project installs native-compatible versions with npx expo install,
regenerates native folders from config instead of hand-editing them, runs on
development builds (not Expo Go) once it has any custom native code, ships OTA updates
only to binaries with a matching runtime version, and is tested on a real iOS and a real
Android device before release.
Before changing anything
npx expo --version && grep -E '"expo"|"react-native"|"expo-router"' package.json
npx expo-doctor # dependency, config, and native-version problems
npx expo install --check # packages out of range for this SDK
ls ios android 2>/dev/null && git check-ignore ios android # CNG (ignored) or committed native dirs?
cat eas.json 2>/dev/null; grep -n "runtimeVersion\|updates" app.json app.config.* 2>/dev/null
| Finding | Meaning |
|---|---|
ios/ and android/ gitignored | Continuous Native Generation: native dirs are build output of app.json + plugins |
| Native dirs committed | Bare workflow: edit native files directly; prebuild would overwrite them |
expo-dev-client installed | Team uses development builds |
No runtimeVersion | OTA updates are unsafe until one is set |
This assumes SDK 54 or newer (New Architecture on by default; legacy architecture support ends after SDK 54). Read the SDK changelog before any upgrade.
Workflow for a feature or fix
- [ ] 1. Check whether the change is JS-only or touches native code/config
- [ ] 2. Install libraries with npx expo install (never bare npm install for native libs)
- [ ] 3. Native config via app.json/app.config.ts + config plugins, not hand edits (CNG)
- [ ] 4. Rebuild the development build if native code/config changed
- [ ] 5. Implement with platform differences handled explicitly
- [ ] 6. Test on iOS and Android devices (and a small screen)
- [ ] 7. Ship: OTA update if JS-only and runtime matches, otherwise new EAS build
Expo Go vs. development builds
| Use | When |
|---|---|
| Expo Go | Learning, prototypes using only libraries bundled in Expo Go |
Development build (expo-dev-client) | Any custom native module, config plugin, push notifications, production parity |
npx expo install expo-dev-client
eas build --profile development --platform ios # or build locally:
npx expo run:ios # requires Xcode; run:android requires Android Studio
npx expo start # then open the dev build, not Expo Go
Rebuild the dev client whenever native dependencies, config plugins, or app.json
native fields change. JS changes only need Metro reload.
Expo Router
app/
_layout.tsx # root Stack; providers; fonts/splash handling
(tabs)/
_layout.tsx # <Tabs>
index.tsx # "/"
settings.tsx # "/settings"
orders/
[id].tsx # "/orders/123"
(auth)/sign-in.tsx
+not-found.tsx
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
const { isSignedIn } = useSession();
return (
<Stack>
<Stack.Protected guard={isSignedIn}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack.Protected>
<Stack.Protected guard={!isSignedIn}>
<Stack.Screen name="(auth)/sign-in" options={{ title: "Sign in" }} />
</Stack.Protected>
</Stack>
);
}
// app/orders/[id].tsx
import { useLocalSearchParams, Stack } from "expo-router";
export default function Order() {
const { id } = useLocalSearchParams<{ id: string }>();
return <><Stack.Screen options={{ title: `Order ${id}` }} />{/* ... */}</>;
}
- Navigate with
<Link href="/orders/42">orrouter.push({ pathname: "/orders/[id]", params: { id } }). - Every route is a deep link automatically (
myapp://orders/42); setschemein app.json. Universal/App Links setup lives indeep-links. useLocalSearchParamsfor the current screen;useGlobalSearchParamsre-renders on every route change — avoid it in heavy screens.Stack.Protected(SDK 53+) replaces redirect-in-effect auth gating.
Configuration and CNG
// app.config.ts
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "Acme",
slug: "acme",
scheme: "acme",
version: "1.4.0",
runtimeVersion: { policy: "appVersion" },
ios: { bundleIdentifier: "com.acme.app", infoPlist: { NSCameraUsageDescription: "Scan receipts to attach them to expenses." } },
android: { package: "com.acme.app", permissions: ["android.permission.CAMERA"] },
plugins: [
"expo-router",
["expo-build-properties", { ios: { deploymentTarget: "16.4" } }],
],
};
export default config;
- With CNG, never edit
ios/orandroid/by hand; changes vanish on the nextnpx expo prebuild --clean. Write a config plugin for anything app.json cannot express. - Permission strings must explain the user benefit; vague strings get App Review rejections.
EXPO_PUBLIC_*env vars are inlined into the JS bundle at build time — public by definition. Secrets stay on your server; use EAS environment variables for build-time config.
EAS Build, Submit, Update
// eas.json
{
"cli": { "appVersionSource": "remote" },
"build": {
"development": { "developmentClient": true, "distribution": "internal", "channel": "development" },
"preview": { "distribution": "internal", "channel": "preview" },
"production": { "autoIncrement": true, "channel": "production" }
},
"submit": { "production": {} }
}
eas build --platform all --profile production
eas submit --platform ios --latest
eas update --channel production --message "Fix totals rounding on checkout"
OTA update rules:
- Updates replace JS and assets only. Any native change (new native library, SDK upgrade, permission, config plugin) requires a new store build.
runtimeVersiondecides which binaries accept an update.policy: "appVersion"ties it toversion;policy: "fingerprint"hashes native dependencies and config so a native change can never receive an incompatible update. Preferfingerprintwhen in doubt.- Publish to
previewfirst, test on an internal build, then production. - Roll back by republishing the last good update group (
eas update:republish) rather than hot-fixing forward under pressure. - Store review rules still apply: OTA must not change the app’s primary purpose.
Native code
- Look for an Expo SDK package or a well-maintained library with a config plugin before writing native code.
- Write custom native modules with the Expo Modules API (Swift and Kotlin):
npx create-expo-module@latest --localscaffoldsmodules/<name>with typed JS bindings. - Third-party libraries must support the New Architecture; check
npx expo-doctorand the React Native Directory before adding one.
Platform differences checklist
| Area | iOS | Android | Approach |
|---|---|---|---|
| Safe areas / insets | Notch, home indicator | Status/nav bars; edge-to-edge is the default on recent Android and SDKs | react-native-safe-area-context useSafeAreaInsets; never hardcode 44/24 px |
| Keyboard | Overlaps inputs | Resize behavior depends on window mode | KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined} or react-native-keyboard-controller |
| Back navigation | Swipe from edge | Hardware/gesture back | Expo Router handles stacks; confirm modals and unsaved-form prompts handle back |
| Shadows | shadow* props | elevation | boxShadow style (New Architecture) for one definition |
| Permissions | Ask once; afterwards only Settings | Can re-ask; “don’t ask again” state | Explain before prompting; deep link to settings on denial (Linking.openSettings()) |
| Fonts | Font family name | File name | Load with expo-font; test both |
| Haptics, blur, date pickers | Native feel differs | Use platform-native components; test on device |
File-level splits: Component.ios.tsx / Component.android.tsx when more than a few
lines differ; Platform.select for values.
Preferred libraries
- Images:
expo-image(caching, placeholders). Lists: FlashList orFlatListwithkeyExtractorand memoized rows (seemobile-performance). - Animation: Reanimated (runs on the UI thread). Gestures: react-native-gesture-handler.
- Secure tokens:
expo-secure-store, never AsyncStorage. Local DB:expo-sqlite(seeoffline-sync).
Gotchas
npm installof a native library picks a version incompatible with the SDK; usenpx expo install, thennpx expo install --fixif expo-doctor complains.- Metro cache after config changes:
npx expo start --clear. - Remote push notifications do not work in Expo Go on Android; use a development build.
- An update published to a channel that no build listens to does nothing — check the
build’s channel in
eas.json. - Running
prebuild --cleanin a project with committed, hand-edited native dirs destroys those edits; confirm with the user before running it.
Verification
npx expo-doctorreports no issues;npx tsc --noEmitand lint pass.- Test on a physical iPhone and a physical Android device using a development or preview build — not only simulators and not Expo Go.
- For OTA: install the preview build, publish to
preview, force-close and reopen twice; the update applies andUpdates.updateIdchanges. - Rotate, open the keyboard on every form, toggle dark mode, and use the largest font size on both platforms.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.