Skip to main content

Configuring Over-the-Air Updates in React Native with Expo Updates

· 8 min read
Engineering Team

Operate Expo updates with explicit runtime compatibility, channels, staged rollout, adoption monitoring, and a recovery strategy for failed JavaScript bundles. This revised guide emphasizes supported APIs, production tradeoffs, and an upgrade-friendly path without tying the advice to a calendar year.

Configuring Over-the-Air Updates in React Native with Expo Updates

Modern implementation baseline

Operate Expo updates with explicit runtime compatibility, channels, staged rollout, adoption monitoring, and a recovery strategy for failed JavaScript bundles.

Let Expo resolve compatible native package versions, use config plugins for native configuration, and validate the final experience in a release build. Updates, icons, splash screens, and runtime policies are release engineering concerns rather than cosmetic afterthoughts.

  • Never publish native-incompatible code to an existing runtime.
  • Promote the same tested update across channels.
  • Monitor failed installs and keep a known-good rollback target.

Production checklist

  • Separate preview and production channels.
  • Test release builds for startup, assets, permissions, and updates.
  • Monitor update adoption and failed launches before broad rollout.

Authoritative references

By the end, you’ll have a React Native app ready to roll out updates seamlessly to iOS and Android users, with separate setups for staging and production environments. Let’s dive in!


Prerequisites

Before we get started, make sure you have these tools ready:

  • Node.js (v20.x or later, LTS recommended): Grab it from nodejs.org.
  • npm (v10.x or later, comes with Node.js).
  • Expo CLI: Our go-to for easy React Native setup.
  • Android Studio: Use the current stable release with compatible Android SDK and NDK tooling.
  • Xcode (16+): For iOS development (macOS only). You’ll need Xcode 16+ (iOS 18 SDK) to meet App Store requirements.
  • A code editor like VS Code.
  • Git: For version control and managing your code.

Got everything? Awesome—let’s set up your project!


Step 1: Create Your React Native Project

Let’s kick things off by setting up a fresh React Native project with Expo.

  1. Create a TypeScript Expo project:

    npx create-expo-app@latest MyApp --template blank-typescript
    cd MyApp
  2. Install Expo Updates with compatible native dependencies:

    npx expo install expo-updates
  3. Configure the project and environment channels:

    eas update:configure
  4. Test the project:

    npx expo start

    Press a for Android or i for iOS (macOS only) to launch the app on an emulator or device. You should see a simple welcome screen.

Summary: You have configured expo-updates with a runtime policy and environment-specific channels.


Step 2: Set Up Expo Updates

Now that your project is ready, let’s configure Expo Updates to handle OTA updates. This step connects your app to Expo’s cloud service, so you can push updates directly to users.

Project Configuration

  1. Update app.json to enable OTA updates:

    {
    "expo": {
    "updates": {
    "enabled": true,
    "url": "https://u.expo.dev/your-project-id",
    "checkAutomatically": "ON_LOAD",
    "fallbackToCacheTimeout": 0
    },
    "ios": {
    "bundleIdentifier": "com.myapp.app"
    },
    "android": {
    "package": "com.myapp.app"
    }
    }
    }

    You’ll get your project-id in Step 3 when we set up EAS (Expo Application Services).

  2. For bare workflows, link expo-updates:

    • iOS: Run npx pod-install to set up CocoaPods dependencies.
    • Android: Add the following to android/app/build.gradle:
      apply from: "../../node_modules/expo-updates/expo-updates.gradle"
  3. Add update-checking logic to src/App.tsx:

    import { useEffect } from 'react';
    import { View, Text } from 'react-native';
    import * as Updates from 'expo-updates';

    export default function App() {
    useEffect(() => {
    const checkForUpdates = async () => {
    try {
    const update = await Updates.checkForUpdateAsync();
    if (update.isAvailable) {
    await Updates.fetchUpdateAsync();
    await Updates.reloadAsync();
    }
    } catch (e) {
    console.error('Update check failed:', e);
    }
    };
    checkForUpdates();
    }, []);

    return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
    <Text>Welcome to MyApp!</Text>
    </View>
    );
    }

    This code checks for new updates when the app starts, downloads them, and reloads the app to apply them.

iOS Configuration

  1. Open ios/MyApp.xcworkspace in Xcode and confirm ExpoUpdates is linked in the Pods target.
  2. Add the EXUpdatesURL key to Info.plist with the value https://u.expo.dev/your-project-id.
  3. Ensure the app’s bundle identifier matches app.json.

Android Configuration

  1. Update android/app/src/main/res/values/strings.xml:
    <resources>
    <string name="expo_updates_url">https://u.expo.dev/your-project-id</string>
    </resources>
  2. Ensure MainApplication.java includes expo-updates:
    import expo.modules.updates.UpdatesPackage;

    public class MainApplication extends Application implements ReactApplication {
    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected List<ReactPackage> getPackages() {
    return Arrays.asList(
    new MainReactPackage(),
    new UpdatesPackage()
    );
    }
    };
    }

Summary: You’ve configured Expo Updates in your app, setting up the foundation for OTA updates. Next, we’ll connect to EAS to manage those updates.


Step 3: Connect to EAS for OTA Updates

With your app configured, let’s hook it up to Expo Application Services (EAS), the cloud platform that powers OTA updates by hosting your JavaScript bundles.

  1. Log in to Expo:

    eas login

    Follow the browser prompt to authenticate your account.

  2. Set up EAS for your project:

    eas build:configure

    This creates an eas.json file:

    {
    "build": {
    "development": {
    "developmentClient": true,
    "distribution": "internal"
    },
    "preview": {
    "distribution": "internal"
    },
    "production": {}
    }
    }
  3. Initialize OTA updates:

    eas update:configure

    This generates a project ID, which you’ll add to app.json under updates.url.

  4. Build your app to test OTA compatibility:

    eas build --profile preview --platform all

    This creates iOS and Android binaries ready for OTA updates.

Summary: You’ve connected your app to EAS, setting up the cloud infrastructure to deliver OTA updates. Now, let’s try pushing your first update!


Step 4: Deploy Your First OTA Update

Now that your app is linked to EAS, let’s deploy an update to see OTA in action.

  1. Make a small tweak to your app, like updating the welcome message in src/App.tsx:

    <Text>Welcome to MyApp! (Updated)</Text>
  2. Publish the update to the production branch:

    eas update --branch production

    This uploads your new JavaScript bundle to EAS.

  3. Test the update:

    • Run your app in preview or production mode (avoid Expo Go, as it may interfere with updates).
    • Close and reopen the app to download the update.
    • Check that the updated message appears.
  4. For a staging environment, publish to a separate branch:

    eas update --branch staging

    Use the EAS dashboard to manage branches or roll back updates if needed.

Summary: You’ve successfully deployed an OTA update, and your app now reflects the latest changes without an app store submission.


Step 5: Set Up Staging and Production Environments

To keep things organized, let’s configure separate OTA updates for staging and production environments, so you can test changes safely before going live.

  1. Create a dynamic app.config.ts for environment-specific settings:

    import { ConfigContext, ExpoConfig } from 'expo/config';

    export default ({ config }: ConfigContext): ExpoConfig => ({
    ...config,
    name: process.env.APP_ENV === 'staging' ? 'MyApp Staging' : 'MyApp',
    slug: 'myapp',
    updates: {
    enabled: true,
    url: `https://u.expo.dev/your-project-id`,
    checkAutomatically: 'ON_LOAD',
    fallbackToCacheTimeout: 0,
    },
    ios: {
    bundleIdentifier: process.env.APP_ENV === 'staging' ? 'com.myapp.staging' : 'com.myapp.app',
    },
    android: {
    package: process.env.APP_ENV === 'staging' ? 'com.myapp.staging' : 'com.myapp.app',
    },
    });
  2. Build for each environment:

    • Staging: eas build --profile preview --platform all
    • Production: eas build --profile production --platform all
  3. Publish updates for each environment:

    APP_ENV=staging eas update --branch staging
    APP_ENV=production eas update --branch production
  4. Test the environments:

    • Install the staging build and verify the staging branch updates.
    • Install the production build and confirm the production branch updates.

Summary: You’ve set up separate staging and production environments, allowing safe testing of updates before they reach all users.


Step 6: Enhance Your OTA Workflow

With your OTA system up and running, let’s explore ways to make it even better.

  • Rollbacks: If an update goes wrong, revert it using the EAS dashboard or CLI:
    eas update:rollback --branch production
  • A/B Testing: Create branches like experiment-a and experiment-b to test new features with different user groups.
  • Analytics: Add expo-analytics to track how users interact with updates and catch errors early.
  • Optimize Updates: Keep update sizes small by excluding unused assets and using code-splitting techniques.

Summary: Enhance your OTA setup with rollbacks, A/B testing, analytics, and optimization for a smooth and reliable update process.


Troubleshooting Tips

  • Updates Not Loading: Ensure you’re not in debug mode, as it skips OTA updates. Test with preview or production builds.
  • iOS Issues: Verify the EXUpdatesURL in Info.plist matches your EAS project ID.
  • Android Problems: Check expo-updates linking in android/app/build.gradle and confirm expo_updates_url is set in strings.xml.
  • EAS Setup Errors: If eas update:configure fails, manually check eas.json and your project ID.

Conclusion

You’ve built a React Native app with Expo Updates, runtime compatibility, and staged channels for safely delivering eligible JavaScript and asset changes. With separate staging and production environments, you’re ready to test and deploy like a pro. Want to speed things up? Explore prebuilt templates with OTA support at Instamobile or Dopebase. For more advanced features, check out Expo’s documentation or join the React Native community at reactnative.dev.

Additional Resources