Skip to main content
Engineering7 min readFebruary 14, 2026

Building Production Apps with Expo and React Native

A practical guide to building production React Native apps with Expo — project setup, navigation, native modules, EAS Build, OTA updates, and deployment patterns.

James Ross Jr.
James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Expo has evolved from a beginner-friendly wrapper around React Native into a legitimate production toolchain. The managed workflow handles the parts of mobile development that drain engineering time — builds, native configuration, OTA updates — while giving you escape hatches when you need direct native access.

I build most of my React Native apps with Expo now. Here is how to set up a production-quality project and avoid the pitfalls.

Project Structure and Configuration

Start with create-expo-app and the latest SDK. Expo's SDK releases are tied to React Native versions, and staying current means fewer compatibility issues with third-party libraries.

Structure your project for maintainability:

src/
 app/ # Expo Router file-based routes
 components/ # Shared UI components
 features/ # Feature-specific modules
 hooks/ # Custom hooks
 services/ # API clients, storage, analytics
 stores/ # State management (Zustand)
 utils/ # Pure utility functions
 constants/ # Theme, config, enums

Use Expo Router for navigation. It brings file-based routing to React Native, similar to Next.js or Nuxt. Screens are defined by their file path, layouts wrap groups of screens, and deep linking works automatically based on the route structure. This eliminates the boilerplate of manually configuring React Navigation stacks.

TypeScript is non-negotiable for production apps. Expo's TypeScript support is first-class. Enable strict mode and use the generated route types from Expo Router — they catch navigation bugs at compile time instead of runtime.

For configuration, use app.config.ts instead of app.json. The TypeScript config file lets you compute values, read from environment variables, and type-check your configuration. Set up environment-specific configs for development, staging, and production using Expo's built-in environment variable support.

Working with Native Modules

One persistent myth about Expo is that you cannot use native modules. This has not been true for years. Expo's development builds and config plugins give you full native access without ejecting.

Expo's built-in modules cover the most common needs: camera, file system, location, notifications, secure storage, haptics, and more. These are well-tested, consistently updated, and cover 80% of native API needs.

When you need a third-party native module, use a development build. Run npx expo prebuild to generate the native projects, then npx expo run:ios or npx expo run:android to build locally with native modules included. This replaces the old "eject" workflow — you get native access while keeping Expo's tooling.

Config plugins let you modify native project configuration without maintaining native code directly. Need to add an iOS entitlement, modify the Android manifest, or configure a native SDK? Write a config plugin that makes the change during the prebuild step. This keeps native configuration declarative and version-controlled.

For custom native functionality — a specialized Bluetooth protocol, a custom video codec — use Expo Modules API to write native modules in Swift and Kotlin that integrate cleanly with Expo's build system. This is preferable to writing raw bridge modules because the API handles serialization and threading.

Building and Deploying with EAS

EAS (Expo Application Services) is Expo's cloud platform for building, submitting, and updating apps. It replaces the pain of maintaining local Xcode and Android Studio build environments.

EAS Build compiles your app in the cloud. You configure build profiles in eas.json — development, preview, and production — each with different signing credentials, environment variables, and build settings. Trigger a build with eas build and it runs on Expo's servers, producing installable artifacts.

For CI/CD, trigger EAS builds from GitHub Actions. On every merge to main, build a preview version and distribute it to testers. On tagged releases, build production versions and submit to the app stores. The build configuration is declarative, which means your CI pipeline is a simple trigger rather than a complex build script.

EAS Submit automates app store submission. It handles the Apple App Store and Google Play submission process, including metadata, screenshots, and review notes. This is where mobile deployment traditionally requires the most manual work, and automating it saves significant time.

EAS Update enables over-the-air JavaScript updates without going through app store review. When you fix a bug or tweak UI copy, push an update that users receive on their next app launch. This is invaluable for fixing critical bugs immediately rather than waiting days for app store review.

However, OTA updates cannot change native code. If your update modifies native modules, adds new permissions, or changes the app binary, you need a full build and store submission. Plan your release strategy around this constraint — keep native changes in versioned releases and use OTA for JavaScript-only fixes. The mobile testing strategy should cover both release paths.

Production Patterns

Several patterns consistently appear in production Expo apps I build.

Error boundaries with crash reporting. Wrap your root layout in an error boundary that catches rendering errors and displays a fallback UI. Integrate Sentry or BugSnag through their Expo plugins for crash reporting — you need visibility into production errors that users do not report.

Secure token storage. Use expo-secure-store for authentication tokens. It maps to Keychain on iOS and EncryptedSharedPreferences on Android. Never store tokens in AsyncStorage. Following mobile security best practices from the start avoids painful retrofits.

Optimistic state updates. When a user performs an action, update the local state immediately and sync to the server in the background. If the server request fails, roll back. This makes your app feel fast regardless of network conditions. Pair this with a Zustand store that separates local state from synced state.

Deep link handling. Expo Router handles deep links based on your file structure. Configure your URL scheme in app.config.ts and set up universal links (iOS) and app links (Android) for web-to-app navigation. Test deep links to authenticated routes — they need to redirect through login if the user is not authenticated, then continue to the intended destination.

Expo has reached the maturity level where it handles the boring infrastructure work so you can focus on building features. The mobile development landscape has enough complexity without also managing Xcode project files by hand. Use the tools that let you ship.