How to implement over the air updates with expo-updates in React Native

I recently started a new Twitch series called React Native Live, hosted on my Twitch channel, and a recent installment was about installing expo-updates. If you missed it, no worries — this blog post will cover the same topic…or watch it on YouTube!
OTA Updates Explained
React Native apps are JavaScript logic bundled up and placed into a native UI. While rolling an update out to the app store might take up to a week, changing that JavaScript “bundle file” can be handled on-the-fly. This is one of React Native’s most exciting innovations! These are usually called over-the-air (OTA) updates. So far, Apple and Google have been okay with this practice, even though it technically can change the functionality of the live app.
In order to swap out this bundle, the app needs to know that there’s an update available, download it from some online source, and then restart the app. You can technically build this all yourself, but most React Native developers use one of two different services: Microsoft’s CodePush or expo-updates.
We’re no strangers to CodePush at Infinite Red (if you’re not familiar with Infinite Red, we’re the world’s best React Native development consultancy), but I was very curious about using expo-updates in a newly “ignited” React Native app. In the live stream, I implemented it and got it working in about two hours. Here’s how I did it!
Getting started with expo-updates
I first spun up a new Ignite app. If you’re not familiar with Ignite, it’s the most popular React Native CLI and boilerplate, and has been for about 5+ years now!
npx ignite-cli new OTAHack
For the purposes of this project I didn’t want to rely on an Expo managed app, so I didn’t pass in --expo
to the CLI. This spins up a new “vanilla” React Native app with all the Infinite Red defaults, including unimodules, which will come into play later. Unimodules lets us use most Expo modules in a regular ol’ React Native app. If you’re implementing this into a non-Ignite app, you’ll need to install unimodules first.
Note: for the following parts of this blog post, I was following the installation instructions found here: https://docs.expo.dev/bare/installing-updates/
Set up in Expo
I logged into Expo (https://expo.dev/) and created a new project called OTAHack.

This allows Expo to host my JS bundle and lets expo-updates retrieve any updated bundles from their servers. I took note of my username (jamonholmgren) and the project slug (OTAHack), as we’ll be setting those in two places below.
Installing expo-updates
When Ignite was finished and I had a newly minted app, I cd
’d into that directory and installed expo-updates from the command line.
cd OTAHack
yarn add expo-updates
Note: this is not a development dependency, as you actually do need to ship it with your production app!
I then updated the app.json
file to add the propersdkVersion
— in this case, 42.0.0
. My actual SDK version was 42.0.3, but expo-updates didn’t seem to like that.

I then went into my index.js at the root of my project and added this line to the top:
import "expo-asset"
You also need to add a line for assetPlugins
to your metro.config.js
if you want to support updating assets on the fly.
module.exports = {
transformer: {
assetPlugins: ['expo-asset/tools/hashAssetFiles'],
// ...
Configuring iOS
My next stop was in the AppDelegate.h in the ios folder. I added a few lines of code to that:

Note: It’s difficult to properly alt-tag these code diff images, so to see the full diff, go here.
The AppDelegate.m
file also needed some changes:


What these changes are doing is changing the way that iOS initializes the React Native app, switching to the EXUpdatesAppController
.
I also added a new folder and plist file at ios/OTAHack/Supporting/Expo.plist
and added this to it:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXUpdatesReleaseChannel</key>
<string>default</string>
<key>EXUpdatesSDKVersion</key>
<string>42.0.0</string>
<key>EXUpdatesURL</key>
<string>https://exp.host/@jamonholmgren/OTAHack</string>
</dict>
</plist>
Note: the
EXUpdatesURL
needed to be set to my Expo username and project name that we set up before.
I opened OTAHack.xcworkspace
in Xcode and then added this new file to the project.


It then showed up in my project under the OTAHack
folder.

Still in Xcode, I went to the main project icon on the left, then under Targets I chose OTAHack, Build Phases, and expanded Bundle React Native code and images. I added this line to the bottom of that shell script:

../node_modules/expo-updates/scripts/create-manifest-ios.sh
And that’s it — I was set up on iOS!
Configuring Android
In the android/app/build.gradle
file (not the one in android/build.gradle
, mind you), I made the following changes:

Here are the lines of code to copy & paste:
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute().text.trim(), "../react.gradle")
apply from: new File(["node", "--print", "require.resolve('expo-updates/package.json')"].execute().text.trim(), "../scripts/create-manifest-android.gradle")
I am not using ProGuard, but just in case, I did add these lines to the end of android/app/proguard-rules.pro
:
-keepclassmembers class com.facebook.react.ReactInstanceManager {
private final com.facebook.react.bridge.JSBundleLoader mBundleLoader;
}
In the android/app/src/main/AndroidManifest.xml
file, I added these lines of code just before the line <activity android:name=".MainActivity"
:
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@jamonholmgren/OTAHack" />
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="42.0.0" />
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="default" />
Note: Just as with the iOS plist, make sure to change the
EXPO_UPDATE_URL
andEXPO_SDK_VERSION
to the proper values for your own use. If you don’t know yourEXPO_UPDATE_URL
, it’s constructed with your Expo username and the project “slug” we set up before.
In the android/app/src/main/java/com/OTAHack/MainApplication.java
file, I made the following changes:

This ensures that Android will use the proper bundle name and also initializes the UpdatesController
.
I was then all set up for Android! Yay!
Configuring React
Now that the native side was all configured and ready to go, I needed to implement the JavaScript side.
In Ignite, the first screen that opens is the Welcome screen. So I opened up app/screens/welcome/welcome-screen.tsx
and added few hooks, like so:

The useEffect sets up a timer that checks every 3 seconds for updates. Clearly, if you have a real production app, this would be ridiculous, so I’d recommend checking on a much longer timer or triggered by something else.
It then says that an update is available in the JSX. You could provide a button that reloads the app (expo-updates comes with a handy Updates.reloadAsync()
function to kick this off) or some other affordance to make it happen. You’d probably want to do an Updates.fetchUpdateAsync()
first, to ensure the update was downloaded.
Running in Release Mode on iOS
You can only test expo-updates in release builds, as dev builds will just use the Metro packager server to update.
If you’re lucky, you might be able to just run npx react-native run-ios --configuration Release
and have it work right out of the gate. If so, skip to the next section.
There are lucky people, and then there’s me. For me, I had to run the iOS build in Xcode to make it build in release.
First, I updated the Signing & Capabilities -> Signing team to be my “Jamon Holmgren” team.

Then I went to the top bar and clicked Edit Scheme…

..and edited the Run scheme to run in Release.

I could then run the app in Release mode on the iOS simulator.
Running in Release on Android
I had a bit better luck with Android by just running the following:
npx react-native run-android --variant=release
This opened up the Android emulator and ran it in release mode.
Publishing updates
By running expo publish
I was able to push up various changes to Expo and watch the hash change, like so:

Success!
What’s next?
Now that you have expo-updates working, you can push out changes to your existing apps over the air.
However, there’s a lot more to learn! You can publish a new release to a certain “channel”, which allows you to push it to select beta testers first to make sure it works. You can roll back releases, set specific releases, and of course there’s a lot to learn about how to notify users that there’s a new release and how to handle the user experience of reloading the app.
You might also have some more questions, like “what happens when an app is two releases behind?” and “what happens to app state when I update?”.
We might write another blog post soon that talks about all of that. Make sure to follow Red Shift (this publication) and we’ll update this blog post if/when that happens.
ORRRrrrr…you can just hire us, the greatest React Native development shop in the world, to handle all this for you. We promise you’ll be in good hands. Just send us an email and we’ll take care of you.
Happy OTA updating!