Using EAS Update in an existing native app
Edit this page
Learn how to integrate EAS Update into your existing native Android and iOS app to enable over-the-air updates.
If your project is a greenfield React Native app — primarily built with React Native from the start, and the entry point of the app is React Native, then skip this guide and proceed to Get started with EAS Update.
This guide explains how to integrate EAS Update in an existing native app, sometimes referred to as a brownfield app. It assumes that you are using Expo SDK 52 and React Native 0.76.
Instructions are not available for older Expo SDK and React Native version. Additional hands-on support for integrating with older versions can only be provided for enterprise customers (contact us).
Prerequisites
The following instructions may not work for all projects. The specifics of integrating EAS Update into existing projects depend heavily on the specifics of your app, and so you may need to adapt the instructions to your unique setup. If you encounter issues, create an issue on GitHub or open a pull request to suggest improvements to this guide.
You should have a brownfield native project with React Native installed and configured to render a root view. If you don't have this yet, follow the Integration with Existing Apps guide from the React Native documentation and then come back here once you have followed the steps.
- Your app must be using the latest Expo SDK version and its supported React Native version.
- Remove any other update library integration from your app, such as react-native-code-push, and ensure that your app compiles and runs successfully in both debug and release on your supported platforms.
- Support for Expo modules (through the
expo
package) must be installed and configured in your project. Learn more. - Your metro.config.js must extend
expo/metro-config
. - Your babel.config.js must extend
babel-preset-expo
. - The command
npx expo export -p android
must run successfully in your project if it supports Android, andnpx expo export -p ios
if it supports iOS.
Installation and basic configuration
Follow steps 1, 2, 3, and 4 from the Get started with EAS Update guide.
After this is complete, you will have installed and authenticated with eas-cli
, installed expo-updates
to your project, initialized an associated EAS project, and added basic configuration to your native projects.
Opt out of automatic setup
The next step is to disable the default behavior of expo-updates
to automatically set itself up in a way that supports greenfield React Native projects.
Disable automatic setup on Android
Modify android/settings.gradle to set the property that disables automatic updates initialization, as in the example below:
Disable automatic setup on iOS
Pass in the environment variable to CocoaPods installation to disable automatic updates initialization.
-Â
EX_UPDATES_CUSTOM_INIT=1 npx pod-install
Set up your React Native app to use expo-updates for loading the release bundle
The next step is to integrate expo-updates
into your Android and iOS projects so that your app will use expo-updates
as the source of your app JavaScript in release builds.
Example
A complete working example is available at this GitHub repository .
Integrating expo-updates with your React Native bundling
-
Ensure that your Metro config extends the Expo config, as in this example:
metro.config.js// Learn more https://docs.expo.io/guides/customizing-metro const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef // Make any custom changes you need for your project by // directly modifying "config" module.exports = config;
-
If you are using a custom entry point, be sure to include Expo initialization there. This ensures that Expo libraries (including
expo-updates
) are all initialized properly. Here are two examples:First Custom Entry Point file example// Expo recommends using registerRootComponent(). // It registers the component with the react-native AppRegistry, // and performs all required Expo initialization // (including expo-updates setup) import App from './App'; import { registerRootComponent } from 'expo'; registerRootComponent(App);
Second custom entry point file example// If you need to keep an existing entry point that uses AppRegistry directly, // you will need to add a call to Expo's initialization before registering the // app, as shown below. import App from './App'; import 'expo/src/Expo.fx'; import { AppRegistry } from 'react-native'; function getApp() { return <App />; } AppRegistry.registerComponent('App', () => getApp());
Integrating expo-updates on Android
The following instructions assume you have an app written in Kotlin, with one or more native activities. Open android/app/src/main/java/com/<your-app-name>/MainActivity.kt and follow the steps below.
- Your React Native activity should subclass
com.facebook.react.ReactActivity
. - In this activity, add code to
onCreate()
to initialize the updates system. The initialization should not happen in the main thread (otherwise a lockup and ANR will occur). - Override
getMainComponentName()
to return the name of the app you registered in your JS entry point above. - Show the React Native view, by overriding the
createReactActivityDelegate()
method as shown below.
package com.yourpackagename
import android.content.Context
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
import expo.modules.updates.UpdatesController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
// Step 1
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.IO).launch {
startUpdatesController(applicationContext)
}
}
// Step 2
private fun startUpdatesController(context: Context) {
UpdatesController.initialize(context)
// Call the synchronous `launchAssetFile()` function to wait for updates ready
UpdatesController.instance.launchAssetFile
}
// Step 3
override fun getMainComponentName(): String = "App"
// Step 4
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
) {})
}
}
Integrating expo-updates on iOS
The following instructions assume you have an app written in Swift, with one or more native screens that have custom UIViewControllers. We will add a custom view controller that renders your React Native app.
AppDelegate changes
- Modify AppDelegate.swift so that it extends
EXAppDelegateWrapper
. - If you are not already doing so, add a public method to get the running
AppDelegate
instance, so that your custom view controller can access it later. - Add a reference to the singleton instance of the
expo-updates
AppController
class, which manages the updates system on iOS. - Override the
bundleUrl()
method to return the correct bundle URL for updates, if the updates system is running. - The
didFinishLaunchingWithOptions()
method needs to perform two steps:- Initialize the root view factory used later to create the React Native root view.
- Call
AppController.initializeWithoutStarting()
. This creates the controller instance, but defers the rest of the updates startup procedure until it is needed.
import ExpoModulesCore
import EXUpdates
import React
import UIKit
@UIApplicationMain
// Step 1
class AppDelegate: EXAppDelegateWrapper {
let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
var launchOptions: [UIApplication.LaunchOptionsKey: Any]?
// Step 2
public static func shared() -> AppDelegate {
guard let delegate = UIApplication.shared.delegate as? AppDelegate else {
fatalError("Could not get app delegate")
}
return delegate
}
// Step 3
var updatesController: (any InternalAppControllerInterface)?
// Step 4
override func bundleURL() -> URL? {
if let updatesUrl = updatesController?.launchAssetUrl() {
return updatesUrl
}
return bundledUrl
}
// Step 5
private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
self.launchOptions = launchOptions
self.moduleName = "App"
self.initialProps = [:]
self.rootViewFactory = createRCTRootViewFactory()
AppController.initializeWithoutStarting()
}
/**
* Application launch initializes the custom view controller; all React Native
* and updates initialization is handled there
*/
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
initializeReactNativeAndUpdates(launchOptions)
return true
}
}
Implementing a custom view controller
- The view controller should implement the updates protocol
AppControllerDelegate
. - The view controller initialization should
- Set the app delegate's updates controller instance, so that its
bundleURL()
method above works correctly for updates. - Set the
AppController
delegate to the view controller instance - Start the
AppController
- Set the app delegate's updates controller instance, so that its
- Finally, the view controller must implement the one method in the
AppControllerDelegate
protocol,appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool)
. This method will be called once the updates system is fully initialized, and the latest update (or the embedded bundle) is ready to be rendered.- Create the React Native root view using the root view factory created by the app delegate. The app name passed in must match the app name that you registered in your JS entry point above.
- Add this root view to the view controller.
import UIKit
import EXUpdates
import ExpoModulesCore
// Step 1
public class CustomViewController: UIViewController, AppControllerDelegate {
let appDelegate = AppDelegate.shared()
// Step 2
public convenience init() {
self.init(nibName: nil, bundle: nil)
self.view.backgroundColor = .clear
// Step 2.1
appDelegate.updatesController = AppController.sharedInstance
// Step 2.2
AppController.sharedInstance.delegate = self
// Step 2.3
AppController.sharedInstance.start()
}
required public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
@available(*, unavailable)
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Step 3
public func appController(
_ appController: AppControllerInterface,
didStartWithSuccess success: Bool
) {
createView()
}
private func createView() {
// Step 3.1
let rootView = appDelegate.rootViewFactory.view(
withModuleName: appDelegate.moduleName ?? "main",
initialProperties: appDelegate.initialProps,
launchOptions: appDelegate.launchOptions
)
// Step 3.2
self.view.addSubview(rootView)
let controller = self
controller.view.clipsToBounds = true
controller.view.addSubview(rootView)
rootView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rootView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor),
rootView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor),
rootView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor),
rootView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor)
])
}
}
Common questions
How long will this take to add to my app?
Assuming you are using the latest version of React Native supported by the Expo SDK, and you are comfortable with the React Native integration in your native projects, then you can likely integrate EAS Update in a similar amount of time as it would take you to integrate with a tool like CodePush or Sentry.
The most important factor is the React Native version that your app uses. If your app uses anything older than the latest supported version by the Expo SDK (as referenced at the top of this guide), then you will want to upgrade to that version first, and the time that will take is heavily dependent on the size and complexity of the app and skill and experience level of the team working on it.
I'm migrating from CodePush, what else do I need to know?
To learn more, see Migrating from CodePush guide.