The right way to (Code)Push React Native

Liran Hershko
Fundbox Engineering
5 min readSep 19, 2019

In the midst of 2018, my team and I were assigned the task of rebuilding Fundbox’s mobile app. Our chosen stack was React Native, as the framework, complemented by Mobx as a state management library. We also wanted to have the ability to add new features and fix bugs rapidly, without needing to submit the app to the different stores. We’ve found CodePush to be exactly what we needed.

Up until now, we’ve managed to deploy to both iOS and Android platforms, with more than 150 successful CodePush releases.

In this post, I will share Fundbox’s approach to CodePush and how we implement it in our React Native app.

CodePush overview

CodePush allows you to change your application’s code, release it, and have your users get the new version almost immediately. It doesn’t require the end-user to upgrade to a new version from the app store.

It’s important to remember that you can only update certain types of files, which I will discuss below.

Continuous deployment of a mobile app

When we initially released our very first React Native app, we wanted to have continuous deployment-like updates to the app, similar to how we, at Fundbox, deploy our web applications. Therefore, we decided that all of our CodePush updates would be configured as mandatory. Thus, when an update is presented, it is installed immediately. There’s no reason for a user to run an old JS bundle when a new one is available, plus, it’s easier for us to manage.

Our initial implementation of CodePush was a simple Higher-Order Component wrapping our App component. It looked like this:

When a mandatory update is available, the internal CodePush sync process kicks in, downloading the new release, installing it, and when it’s done, the app is restarted with the new updated JS bundle. This implementation was far from user-friendly. Let me explain why.

Unwanted UI updates at runtime

The first screen that many users see, is our login screen asking for credentials for their Fundbox account. With our initial naive implementation, after a CodePush release a user who had opened the app and started typing their credentials suddenly experienced a refresh; the typed credentials were erased and had to be entered again. That, of course, was the result of CodePush’s asynchronicity. The updated bundle was downloaded and installed while the user was engaging with the UI.

Preventing UI updates at runtime

In order to fix that behavior, we utilized CodePush’s sync method in conjunction with Mobx’s reaction to changes in the update state.

Then, on our LaunchRoute.js, we simply listen to the CodePush sync status updates. When the sync is done, the appReady observable is set to true and we can navigate the user to the auth route where they can enter their credentials.

Now, when the user opens the app, the splash screen navigates away only after the sync process is done.

Traditional release flow

If you’ve ever released a mobile app or you’ve practiced CI/CD in web development, you might have a deployment flow that generally includes these steps:

  1. Branch out of master branch (version branch for mobile apps) to a feature branch.
  2. Commit some code changes.
  3. Review and perform tests.
  4. Release the feature branch as the new version of your app.
  5. Merge back to the master branch.

That works as well for web development as it does for mobile apps, as the generated ipa/apk file is bundled with the code changes you’ve made. As for a CodePush release though, this flow isn’t enough. Let’s see why…

CodePush release caveat

Earlier I mentioned that a CodePush release only bundles certain file types.
In a React Native app, this includes the various UI components and other JS files that support their functionality. Thus, changes to the native code as AppDelegate.m, MainActivity.java or even adding a new plugin, is not included in the CodePush bundle to be released.
Think about clients out there running native code that is agnostic to a new plugin you’ve added. Once you deploy a CodePush bundle that tries to call the newly added plugin, your app would crash.

How can we make sure that the next CodePush release won’t break the app?

Solid CodePush release flow

A CodePush release should be tested and deployed in a different manner than the traditional flow described above.

Your goal is to test the way your users experience the new CodePush release update. In order to do that, you should push — or code-push to be exact — the feature branch to a dedicated staging/testing CodePush environment. Then, instead of performing the tests on the feature branch itself, you should test it against the branch that represents the released native code.

Remember that your users are currently running a bundled output of the branch you initially branched from — before making any changes to the code, this is the version branch. After code-pushing the feature branch to a staging/testing CodePush environment, you’ll be able to simulate the experience of an incoming CodePush bundle, exactly as your users will experience it.

Here’s an example:

Assume you have your application (v1.0.0) released with a CodePush bundle version 1. This can be described as v1.0.0–1.

Now, you want to release CodePush version 2 after making some code changes. You also want to make sure that the CodePush release won’t break any of your live v1.0.0–1

  • Branch out of your v1.0.0–1 branch to a feature 1.0.0–2 branch.
  • Commit some code changes. If any native changes are introduced, your app will break while running tests later.
  • Review and perform tests.
  • Make a CodePush staging/testing release from the 1.0.0–2 feature branch.
  • Switch your local branch back to 1.0.0–1 to run the same codebase as your users do.
  • Cleanup — this is how you make sure to remove all possibly installed plugins and cached files before building the project.
cd <project_root># cleaning node_modules and cache:
watchman watch-del-all
rm -rf ~/.rncache node_modules $TMPDIR/react-* $TMPDIR/metro* $TMPDIR/haste-*
npm i
# iOS:
rm -rf ios/Podfile.lock ios/Pods ios/build
cd ios && pod install
cd <project_root>
# Android:
rm -rf android/app/build android/.gradle
# Running Metro bundler with reset-cache:
npm start — — reset-cache
  • Run the app in a staging/testing environment. The new CodePush bundle you previously released will be downloaded and installed.
  • Run tests. If everything passes then you know you have a solid CodePush release.
  • Promote the CodePush release to production.

Wrapping up

CodePush gives you plenty of wiggle room when it comes to fixing bugs and making code changes. It provides great flexibility for handling code updates and gives you control over when to install them. That being said, it also adds another dimension of complexity. So you should simulate and carefully test the way your users will experience the upcoming release update.

--

--