agentleFS
suleymansuleyman/fullstack-agent-skills/skills/expo-react-native/SKILL.md

expo-react-native

Skill2 starsChanged 37 days agoMITView on GitHub

Opinionated rules for production Expo and React Native apps using Expo Router, TanStack Query, Zustand, EAS Build, EAS Update, and EAS Submit, including secure token storage, push notifications, deep linking, permissions, native dependencies, and App Store / Play Store release workflows. Use when building or reviewing Expo screens, navigation, data fetching, auth, notifications, links, or permissions, when adding a native dependency, or when deciding whether a change ships as an OTA update or a new store build.

2 other projects use a version of this.

Adopted by

Most recently taken up by

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 2

    1. CrowPus/Zelyq★ 10same name
    2. sagawrr/skills★ 0same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository5 other files it gives its agents.

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. About 0 are common, 6 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Development builds, not Expo Go: Use Expo Go for throwaway prototypes only.

    line 27
  2. Distinctive:

    Mobile networks fail: Every screen handles loading, empty, error-with-retry, and offline.

    line 31
  3. Very specific:

    When you persist the query cache for offline reads (PersistQueryClientProvider), set maxAge, use a buster tied to the app version, and …

    line 71
  4. Distinctive:

    Use OTA updates for fixes and improvements within the reviewed app.

    line 88
  5. Very specific:

    Use expo-notifications in a development build, and test on physical devices.

    line 94
  6. Very specific:

    Use a custom scheme for development and internal links.

    line 103
  7. Distinctive:

    Treat link parameters as untrusted input.

    line 104
  8. Very specific:

    Test cold start, warm start, signed-out, and unknown-route paths: npx uri-scheme open <url> --ios|--android, xcrun simctl openurl booted …

    line 105
  9. Very specific:

    Remove permissions that libraries add transitively and you don't use (android.blockedPermissions).

    line 113
  10. Distinctive:

    An update isn't applied: check the build's channel and runtime version against the update's.

    line 133
  11. Very specific:

    Works in development, crashes in release: look for missing env at bundle time, dev-only code paths, or Hermes/minification issues.

    line 134
  12. Distinctive:

    Run animations on the UI thread with Reanimated.

    line 141
  13. Distinctive:

    Avoid certificate pinning unless your threat model demands it.

    line 151

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Expo / React Native

Build iOS and Android apps with Expo (development builds, Continuous Native Generation), Expo Router, TanStack Query, and Zustand, shipped through EAS. The app consumes the same API as the web app (see fullstack-architect).

When to use

  • Building screens, layouts, and navigation with Expo Router.
  • Fetching, caching, and mutating API data. Handling auth tokens.
  • Adding or upgrading a dependency, especially one with native code.
  • Push notifications, deep links and universal links, or runtime permissions.
  • Preparing a release: EAS Build, EAS Update, EAS Submit, versioning, rollout, rollback.

Core rules

  1. EAS Update and native builds are separate deployment mechanisms. A native build (EAS Build → store review) ships the native runtime. An update (EAS Update) ships JavaScript and assets to builds with a matching runtime version. Classify every change before shipping. If it touches native code or native config, it needs a new build. Updates cannot add native code.
  2. Verify whether every new dependency needs a new development build. A package with native code (iOS/Android sources, or a config plugin that changes native projects) means a new dev build for the team, a new runtime version, and a store release before any JavaScript that uses it can go out over the air.
  3. Continuous Native Generation. ios/ and android/ are generated from app.config.ts and config plugins (npx expo prebuild). They're gitignored and never hand-edited. Native customization goes through config plugins.
  4. Development builds, not Expo Go. Use Expo Go for throwaway prototypes only. Push notifications, custom native modules, and many config plugins need a dev build (expo-dev-client).
  5. State ownership matches the web: TanStack Query for server state, Zustand for client UI state, route params for navigation state, SecureStore for secrets. Never put server data in Zustand.
  6. Tokens live only in expo-secure-store (Keychain/Keystore). Never in AsyncStorage, unencrypted MMKV, persisted Zustand, or the query cache.
  7. The client is untrusted and old. Binaries stay installed for months. The API must stay backward compatible, the app must tolerate unknown fields and enum values, and a minimum-version check must be able to force an upgrade. The API enforces authorization, never only the app.
  8. Mobile networks fail. Every screen handles loading, empty, error-with-retry, and offline. Requests have timeouts. Retries back off and never retry 4xx.

Architecture

mobile/
├── app.config.ts              # dynamic config: per-environment IDs, plugins, runtimeVersion
├── eas.json                   # build profiles → channels
├── app/                       # Expo Router routes only (thin)
│   ├── _layout.tsx            # providers: QueryClient, session bootstrap, splash
│   ├── sign-in.tsx
│   ├── (app)/
│   │   ├── _layout.tsx        # Stack.Protected guard={isSignedIn}
│   │   ├── (tabs)/{_layout,index,orders}.tsx
│   │   └── events/[id].tsx
│   └── +not-found.tsx
└── src/
    ├── features/<feature>/{api.ts, components/, hooks/, lib/}
    ├── lib/
    │   ├── api/               # fetch client: auth header, single-flight refresh, timeouts, errors
    │   ├── auth/              # SecureStore token storage, session store
    │   ├── query-client.ts    # QueryClient + focus/online managers
    │   └── env.ts
    └── components/
  • Route files stay thin. They read params, validate them, and render a feature screen.
  • Every route is a deep link. A screen loads its own data from the ID in its params. Never pass whole objects through route params or rely on in-memory state from the previous screen. To render instantly from list data, seed the detail query with placeholderData from the list cache.
  • Route params are untrusted strings, and can be string[]. Validate them before use.
  • Gate auth in one layout with Stack.Protected guard={...} (or <Redirect>), not per screen. Keep the intended destination so a signed-out deep link resumes after sign-in.
  • Install with npx expo install <pkg>, which picks SDK-compatible versions. Run npx expo-doctor after upgrades and before release builds.

Client setup code (QueryClient with focus/online managers, token storage, a fetch client with single-flight refresh, push registration, notification taps) is in references/client-setup.md.

Data fetching

  • Wire focusManager to AppState and onlineManager to NetInfo, so queries refetch on foreground and pause offline.
  • Infinite lists use useInfiniteQuery with initialPageParam and getNextPageParam: (last) => last.nextCursor ?? undefined, rendered with FlashList onEndReached. Guard with hasNextPage && !isFetchingNextPage.
  • Pull-to-refresh calls refetch() and tracks its own refreshing state, so focus refetches don't show the spinner.
  • Normalize noisy inputs before they become query keys: debounce and trim search text, round GPS coordinates (3 decimals ≈ 110 m). Otherwise every keystroke or small location change creates a new cache entry and a new request.
  • When you persist the query cache for offline reads (PersistQueryClientProvider), set maxAge, use a buster tied to the app version, and exclude sensitive queries with shouldDehydrateQuery.
  • Sign-out clears everything: SecureStore, the session store, queryClient.clear(), the persisted cache, and the server-side push token registration.

Releases: EAS Build vs EAS Update

ChangeShips via
JS/TS logic, styling, copy, JS-imported imagesEAS Update
Adding or upgrading a package with native codeNew build
Config plugin changes, permissions or usage strings, icon, splash, bundle ID, scheme, associated domains, intent filtersNew build
Expo SDK or React Native upgradeNew build
EXPO_PUBLIC_* value changeEAS Update (values are inlined at bundle time, so publish with the right environment)
  • Set runtimeVersion: { policy: "fingerprint" }, so the runtime version changes automatically whenever the native layer changes. An update then can't reach an incompatible binary.
  • Map channels to build profiles (development, preview, production). Test every update on a preview build with the production runtime before publishing to production.
  • Roll out gradually: eas update --rollout-percentage=10, widen it with eas update:edit, revert with eas update:revert-update-rollout, or roll back fully with eas update:rollback.
  • Upload source maps to your error tracker for every build and every update. Without them, stack traces from OTA bundles are unreadable.
  • Use OTA updates for fixes and improvements within the reviewed app. Features that materially change the app go through store review.

Full workflow (eas.json, app.config.ts, versioning, store submission, staged rollouts) is in references/release-workflow.md.

Push notifications

  • Use expo-notifications in a development build, and test on physical devices.
  • Ask for permission in context, after an action the notification will serve ("Notify me when someone replies"), never at first launch. iOS shows the system prompt only once.
  • Android: create a notification channel before requesting a token. Android 13+ requires the runtime permission.
  • Register the token with the API (PUT /v1/me/devices/{installationId}) on every launch and on token change, and remove it on sign-out. The server stores one row per device.
  • The server sends through the Expo Push Service (or APNs/FCM directly), checks push receipts, and deletes tokens that report DeviceNotRegistered.
  • The payload carries a route and an ID ({ url: "/orders/123" }). Handle taps on cold start and warm start, and validate the path before navigating. Never put sensitive content in the payload, because it shows on the lock screen.

Deep linking

  • Use a custom scheme for development and internal links. Production links that people share use universal links (iOS associatedDomains + apple-app-site-association) and Android App Links (intentFilters with autoVerify + assetlinks.json). Any app can claim a custom scheme.
  • Treat link parameters as untrusted input. Validate IDs, and never perform a state-changing action straight from a link without user confirmation.
  • Test cold start, warm start, signed-out, and unknown-route paths: npx uri-scheme open <url> --ios|--android, xcrun simctl openurl booted <url>, adb shell am start -W -a android.intent.action.VIEW -d <url>.

Permissions

  • Request at the moment of need, after an in-app explanation screen.
  • Handle every state: granted, denied with canAskAgain (ask again later), blocked (canAskAgain: false, offer Linking.openSettings()), iOS limited photo access, and approximate location.
  • Set specific iOS usage strings through config plugin options (e.g. expo-camera's cameraPermission). Vague strings get rejected in review.
  • Request background location only if the core feature can't work without it. Both stores apply extra review.
  • Remove permissions that libraries add transitively and you don't use (android.blockedPermissions).

Patterns to avoid

AvoidDo instead
Shipping a native dependency change via eas updateNew build with a new runtime version (fingerprint policy)
Editing ios/ or android/ by hand in a CNG projectConfig plugins
Tokens in AsyncStorage, MMKV, or persisted Zustandexpo-secure-store
Secrets in EXPO_PUBLIC_* or extraThe API holds secrets. Anything in the bundle is public
ScrollView + .map() for unbounded listsFlashList (or FlatList) with stable keys
Passing objects through route paramsPass IDs and load or seed from the query cache
Permission prompts at first launchIn-context requests after an explanation
Login in an embedded WebViewSystem browser via expo-auth-session + PKCE, or native provider SDKs
fetch without a timeoutAn AbortController timeout in the API client
Profiling a dev buildProfile release-like builds (preview profile)

Debugging

  • "Native module not found" / Cannot find native module: the JS references native code missing from the installed binary. Rebuild the dev build, and confirm the runtime version of OTA updates.
  • An update isn't applied: check the build's channel and runtime version against the update's. With default settings, an update downloads on launch and applies on the next launch.
  • Works in development, crashes in release: look for missing env at bundle time, dev-only code paths, or Hermes/minification issues. Reproduce with a preview build and read symbolicated crash reports.
  • Push token is null: simulator, missing projectId, denied permission, or (Android) no notification channel created.
  • Universal link opens the browser: the AASA or assetlinks.json file isn't served correctly (exact path, application/json, no redirects), or the app ID or signing fingerprint doesn't match.

Performance

  • Long lists: FlashList, memoized row components, stable keyExtractor, and server-provided thumbnails at display size (don't render 4000 px images in 80 px avatars). Use expo-image for caching.
  • Run animations on the UI thread with Reanimated. Don't animate with JS-thread state updates.
  • Startup: keep the root layout light, keep the splash screen visible (expo-splash-screen) until fonts and session restoration finish, and defer non-critical SDK initialization.
  • Select narrowly from Zustand and keep list item callbacks stable.
  • Measure on a low-end Android device with a release build.

Security

  • Refresh tokens live in SecureStore with keychainAccessible: AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY. Access tokens live in memory only.
  • OAuth uses Authorization Code + PKCE through the system browser, or native Sign in with Apple / Google SDKs whose ID token the API verifies. Never ship client secrets in the app.
  • Everything in the JS bundle and app config is readable. Ship only public identifiers.
  • Avoid certificate pinning unless your threat model demands it. A rotated certificate can brick installed apps.
  • Jailbreak/root detection doesn't protect server data. Server-side authorization does.

Testing

  • Unit: Jest with the jest-expo preset for pure logic and hooks.
  • Components: React Native Testing Library. Cover loading, empty, error, offline, and permission-denied states.
  • E2E: Maestro flows for sign-in, the core feature, a deep link cold start, and a push tap, run on release-like builds.
  • Manual pass before each store release: a physical low-end Android, a small iPhone, slow network (Network Link Conditioner), permission denial paths, and an OTA update applied to the release build.

Production-readiness checklist

  • Development builds are in use. npx expo-doctor passes.
  • runtimeVersion uses the fingerprint policy. Channels map to build profiles.
  • Per-environment bundle IDs and package names so builds coexist, with separate push credentials.
  • Tokens are only in SecureStore. Sign-out clears tokens, caches, and the push registration.
  • No secrets in EXPO_PUBLIC_* or the app config.
  • Every screen handles loading, empty, error-with-retry, and offline.
  • Deep links are verified on cold and warm start, signed in and signed out. AASA and assetlinks.json are deployed.
  • Permission requests are in context, denial paths are handled, and usage strings are specific.
  • Push: channel created, permission timing set, token registration and rotation work, receipts are processed on the server.
  • Error tracking has source maps for builds and updates.
  • A minimum supported version check is wired to an update screen.
  • Privacy manifests, App Privacy labels, and the Play Data safety form match actual data collection. In-app account deletion exists if accounts can be created.
  • The release build is tested on physical devices before submission.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.