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

Jamon Holmgren
Red Shift
Published in
8 min readSep 20, 2021

--

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:

Diff here.

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 and EXPO_SDK_VERSION to the proper values for your own use. If you don’t know your EXPO_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:

Diff here.

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:

Diff here.

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!

--

--

Co-founder & CTO @infinite_red. Lutheran, husband, dad to 4, React Native Radio podcast host, Twitch streamer, hockey goalie. Talking shop!