CodePush in React Native

A look at how you can optimise your configuration and UI for a better UX.

Alex Borton
Creating TotallyMoney
6 min readNov 13, 2023

--

CodePush is a really powerful tool that allows us to update our users quickly with iterative changes, without requiring a Store update.

CodePush has some limitations (which I won’t get into here) but it also comes with a lot of configurability, which you may not have dived into.

We recently made a number of changes to allow us to fine-tune our users experience, as well as make sure they are kept up to date with important (and frequent) changes in our app; TotallyMoney.

CodePush default behaviour

If you follow the setup guides that CodePush provide, you’ll end up with the default setup. This means that users effectively require two cold starts to first download/install, then apply/run the latest applicable CodePush update.

Why is this not suitable for our users?

We release changes frequently. Usually, multiple CodePush changes a day, by multiple missions. These range from new features to bug fixes. As much as we would love our users to engage multiple times a day, our app simply doesn’t lend itself to that kind of usage. So, if users aren’t that frequent, it means they will be much slower to adopt our latest changes, especially if the majority of our updates come via CodePush.

Ultimately, that means users aren’t getting the most from our latest features and we aren’t able to promote those features as freely and as quickly as we would like.

How have we managed this until now?

Generally, we have leaned quite heavily on Store updates (App Store and Play Store). This involves frequent submissions and releases. Each release takes time to build (even with a lot of automation) and even more time for some Stores to accept them (ehem… Apple). Not all users have automatic updates enabled, and even if they do, there are conditions outside our control that mean we can’t really know when that update will land in our users hands.

All of those things lead to unpredictability and uncertainty.

On top of that, there is the “back in time” paradigm that means that a user who was on the latest applicable CodePush version could have their app updated by the operating system to the latest Store (or binary) version that was built before the latest CodePush version meaning they now have technically out of date code until the next (or even previous) CodePush is reapplied, with the Cold start rules we talked about earlier. 🤯

What is the ultimate objective of our CodePush changes?

We want to make sure our users receive our updates as soon as possible. This means two things;

  • Change the logic surrounding background checks for CodePush versions to be more assertive.
  • Intercept new downloads/installs/store-updates to apply the latest applicable CodePush.

In other words, we need to handle both Cold and Warm starts. Let’s start to look at some coded solutions.

First, a really quick win

We can swap our default installMode: ON_NEXT_RESTART for installMode: ON_NEXT_SUSPEND and add a minimumBackgroundDuration: 600 // 10 minutes .

Depending on how you have set up your CodePush (using sync or component wrapping), this will sit in the top-level configuration, something like the one below.

import codePush from 'react-native-code-push'

...

const App = codePush({
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.ON_NEXT_SUSPEND,
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
minimumBackgroundDuration: 600,
})(MainAppComponent)

export default App

When the app is “resumed” (comes into the foreground), a check will be made for an applicable CodePush version. If one is found, it will be downloaded to the device immediately. Then, when the user next “suspends” the app for a minimum of 10 minutes, the app will automatically be restarted in the background, with the latest version applied.

As far as users are concerned, nothing has happened here. For us specifically, this ties into a session timeout so our users would expect to be back to authentication level anyway — which is what happens after the background app restart.

This does a decent amount of heavy lifting for us, but doesn’t help with the second scenario of new app downloads from their respective stores.

Manually managing CodePush

CodePush gives us a few different options for manually managing how and when different CodePush actions are performed. This gives us complete control. And it’s actually not as technical as you might think.

To help us with this, I wrote a custom hook that allows us to tap into the CodePush lifecycle events using the CodePush sync method. This has the added benefit of being able to report usage statistics and drop offs too.

Let’s dive in.

Most of the code there is self-documenting, or commented for convenience. Once you have the hook in place, you can use it wherever is convenient, passing in different configurations if required and listening to the various callbacks and status changes.

Something important to note; you need to inform your CodePush component wrapper that you are manually checking for updates, otherwise the two can collide, resulting in failed sync attempts;

import codePush from 'react-native-code-push'

...

const App = codePush({
checkFrequency: codePush.CheckFrequency.MANUAL,
})(MainAppComponent)

export default App

With this additional level of control, we can enforce the second part of our requirements; Intercept new downloads/installs/store-updates to apply the latest applicable CodePush.

Intercepting fresh installations

Our setup has a great spot for intercepting and informing the user that an update is occurring. We will call this a “forced update” because our UI doesn’t allow them to continue with the app until they have finished installing*

*We will fine tune this later as that might not always be desirable for our users.

An important distinction; CodePush has a “mandatory” update. This is different to our approach, purely because we control the users experience from start to finish

We will catch users from a cold start after the Splash Screen has finished, seamlessly transitioning to our Installing UI. This gives accurate feedback to the user as to what is happening and that progress is being made.

Example of the Cold Start updating flow

Let’s take a look at some code.

  // bootsplash screen/controller

// Use some state to decide when CodePush is done
const [isSetupComplete, setIsSetupComplete] = useState(false)
// Similarly, differentiate between Downloading or not
const [isCodePushDownloading, setIsCodePushDownloading] = useState(false)
// retain Download progress for our UI
const [downloadProgress, setDownloadProgress] = useState(null)

const handleCodePushSync = useCodePushSync()

useEffect(() => {
const init = async () => {
await handleCodePushSync({
syncOptions: {
// We tell the custom hook to immediately apply any update.
// this will cause the app to restart when the update is successful.
installMode: codePush.InstallMode.IMMEDIATE,
},
onDownloadStart: () => setIsCodePushDownloading(true),
onDownloadProgress: (progress) => {
setDownloadProgress(
Math.round((progress.receivedBytes / progress.totalBytes) * 100)
)
},
onError: () => setIsCodePushDownloading(false),
})

setIsSetupComplete(true)
}
init()
}, [])

The above implementation starts with setting up our custom hook useCodePushSync() and tapping into the lifecycle methods to firstly see if there is a CodePush update to download, then when that download starts and what the progress is of said download. Finally, when the whole sync process has finished, so we can advance the user to the app via an app restart (to apply the new JS bundle).

Setting some local state in this component, we are easily able to change the UI accordingly, passing in the download progress to a loading bar, for example.

Summary

The combination of the items above means that we have two different update methods;

  • Cold starts/fresh installations will immediately download and apply any available update.
  • Warm starts/app resume will check for and download an update, applying it at the next possible app suspension (or next Cold start).

This setup is configurable too — you can change most of the options using the custom hook and tweak it further to your requirements.

Bonus reading — Manual CodePush Sync

Things get a little more complicated here as we want the option to time out of the forced update for various reasons. For example, if the user is on a bad connection, or they have entered the app from a specific source, or if it’s just taking too long.

In order to achieve that, I had to manually write the sync function, defining our own lifecycle events to make sure the app wouldn’t suddenly restart at a point in time we weren’t anticipating — this didn’t seem possible with the sync method.

Following a very similar pattern, with a really similar API, I created the following hook.

Slightly more advanced usage here allows us to manually decide when to restart the app, bail from an update or continue in a different way with the update — in the background — on the next suspend for example.

More summary

There are many different ways we can configure CodePush and manually call and intercept the lifecycle methods that should give us a level of granularity to suit all our needs!

--

--

Alex Borton
Creating TotallyMoney

Senior engineer making apps and websites. Keen surfer. Leather enthusiast. Moustache connoisseur