Expo Platform Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Pin the SDK at scaffold time: Use npx create-expo-app@latest --template default@sdk-57 (or equivalent) so expo starts at ~57.0.4 - upgrading later is harder than starting on the current supported SDK.
Run expo-doctor after every scaffold or upgrade: npx expo-doctor catches mismatched native module versions, invalid app.json fields, and config drift before you merge an upgrade branch.
Always use npx expo install for Expo packages: Manual npm install can pull JS stubs that do not match the native binaries for SDK 57 - compile success does not guarantee runtime stability.
Treat app.config.ts as the source of truth: Dynamic config supports environments, typed helpers, and conditional plugins; static app.json alone does not scale past one build flavor.
Never commit secrets to extra or EXPO_PUBLIC_*: Prefix public vars with EXPO_PUBLIC_ only for values safe to ship in the client bundle; server secrets belong in EAS Secrets.
Audit the public manifest before release: Run npx expo config --type public and confirm no private keys, internal URLs, or staging endpoints leaked into the build-time config.
Read config at runtime via Constants.expoConfig: Do not import app.json in feature code - the processed manifest from Expo's config plugin pipeline is what actually ships.
Start with Expo Go, graduate to dev builds: Expo Go is fine for learning and demos; add expo-dev-client as soon as you depend on native modules not bundled in Expo Go.
Rebuild the dev client after every SDK bump: OTA updates (eas update) change JavaScript only - expo-dev-client native shells must be rebuilt with eas build --profile development.
Register the root with registerRootComponent: Bare or brownfield entry points that skip expo bootstrap miss autolinking initialization and fail mysteriously when calling native modules.
Prefer requireNativeModule from expo in apps: Library packages should import from expo-modules-core to avoid peer-dependency cycles; app code can use the expo re-export.
Map EAS build profiles to environments: Use eas.json environment keys (development, preview, production) so credentials and env vars resolve consistently in CI and locally.
Pull env vars with eas env:pull: Sync team-managed secrets into .env.local for local dev without copying values into chat or shared drives.
Upgrade SDKs sequentially: Jumping multiple SDK versions in one PR compounds breaking changes - step 54 → 55 → 56 → 57, reading each changelog between merges.
Delete stale native folders after CNG upgrades: Committed ios/ and android/ from an older SDK block prebuild templates for RN 0.86 - gitignore them and let EAS Build regenerate.
Express native changes as config plugins: Hand-editing generated Info.plist or AndroidManifest.xml files is lost on npx expo prebuild --clean (the SDK 57 default).
Write idempotent config plugins: Plugins that append regex replacements twice corrupt native projects - design plugins to be safe on repeated prebuild runs.
Set owner for organization projects: Omitting "owner": "org-slug" in app.config routes builds and credentials to a personal account instead of the team.
One projectId per app binary: Reusing an EAS project across unrelated bundle IDs collides credentials, update channels, and environment variables.
Default engineers to Developer role: Reserve Owner for bus-factor coverage (two owners max); Admin for leads who manage billing and invites - not every IC.
Use robot tokens for CI, not personal passwords: Organization-scoped robot users survive employee departures and provide an auditable revocation path.
Keep slug flexible, projectId stable: Rename display slugs when branding changes; never rotate projectId - EAS services key off the UUID for the app's lifetime.
Match MainComponent / moduleName to JS registration: Bare apps whose native entry says "MyApp" while JS registers "main" show a blank screen with no obvious JS error.
Release SharedObjects explicitly: Long-lived native handles from Expo modules (players, sensors) leak memory until release() or the package hook cleans up on unmount.
Read SDK 57 and RN 0.86 release notes before upgrading: Hermes, Reanimated, and New Architecture defaults change behavior even when TypeScript still compiles - treat the changelog as part of the upgrade checklist.
app.config.ts and config plugins, not hand-edited ios//android/ folders.npx expo-doctor with zero failures.eas build --profile development on CI.EXPO_PUBLIC_API_URL tied to an EAS preview environment - not hardcoded in source.eas env rather than maintaining parallel app.config files.eas build for production after upgrading SDK 57; use OTA only for JS/asset changes within the same runtime version.extra or non-prefixed env vars that get inlined into the JS bundle.npx expo config --type public before every release.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 9, 2026