Our journey from React Native to Expo for mobile app development at Alan 🛤️

Xavier Seignard
Alan Product and Technical Blog
7 min readNov 14, 2023
A meme representing a gaussian curve, on left an Expo beginner, on middle a developper struggling with a large heterogeneous tooling, on right a seasoned Expo developper disguised as a Starwars Jedi.
Credit: internet

At Alan, we’ve always been committed to providing the best user experience possible, and this extends to our own engineers, through what we call the developer experience. We believe that a great developer experience is essential for building high-quality software and that it’s a key factor in attracting and retaining top talent.

We’ve always used React Native for our mobile applications, as it’s a powerful tool that brings the development experience of mobile closer to a web one and reduces concerns about native code and discrepancies between iOS and Android.

However, we recently made a significant shift in our mobile app development process by migrating from React Native to Expo. This blog post will detail our journey and the reasons behind this transition.

Why did we choose Expo? 🤔

Expo is an ecosystem of tools on top of React Native, enabling us not to worry about the native part of our apps in most cases and to have a web-like developer experience. It significantly simplifies the development process by eliminating the need for local compilation of iOS and Android apps and allowing engineers to focus on building and maintaining features for our members.

While we consider engineers to be fullstack at Alan, we noticed a certain shortage on native mobile development skills. As we wanted to make it easier for engineers to contribute to our mobile apps, Expo was the perfect solution for this.

Not that long ago, Expo shipped two major features that made it a viable option for us: expo-dev-client and expo-config-plugins, but before diving into these features, let's take a look at the anatomy of a React Native app.

Anatomy of a React Native app 🦴

The code of a React Native application lives in two realms:

  • JavaScript that runs the core logic of the program and is typically where developers spend their most time.
  • Native Modules are employed to tap into native code when certain capabilities cannot be executed in JavaScript (think cameras, file system, etc.).

The JavaScript code is bundled into a single file, which is then loaded by the native code. The native code is compiled into a binary that is then installed on the device.

A quick analogy with web development would be the following:

  • JavaScript is the code that runs in the browser.
  • Native Modules are the browser APIs (think about navigator.getUserMedia or navigator.bluetooth).

Expo Dev Client 🚀

From this analogy, what we need is a customized app that can load our JavaScript bundles and has all the native capabilities we need.

And this is exactly what Expo Dev Client does. But to understand that, we need to take a step back and look at how Expo works.

Continuous Native Generation 🔄

Maintaining, scaling, and updating a single native project is challenging, let alone maintaining multiple in a cross-platform app. The task becomes even more complex when considering the scaling of the project, increasing third party dependencies, and keeping up with the latest OS releases. This results in slowing down the development momentum, discouraging the addition of complex native functionality, thereby leading to the generation of less powerful apps.

To tackle these challenges, a process called Continuous Native Generation (CNG) is implemented. CNG is an abstract concept that defines the generation of native code from several inputs, typically the fusion of a code template and configuration. The result is a native project that can be compiled into a native app. It is a process used by Expo Prebuild to generate native projects for React Native apps, but the concept extends beyond React Native or cross-platform apps.

Expo Config Plugins 🔌

Expo Config Plugins are a way to customize the native code for your project during the prebuilding process. They provide a way to add custom native code and manage native dependencies for your project.

Expo Config Plugins are particularly useful when your project requires custom native code.

This can be for a variety of reasons, such as:

  • You need to use a library that requires custom native code.
  • Your feature requires custom native code.
  • You need to customize a native behavior for your project.

The Migration Process 🐜

We expected the process of migrating 4 apps to Expo to be a long and arduous one.

With that in mind we identified two major priorities:

  • Avoid a divergent migration branch
  • Identify and classify native code

Divergent migration branch / merge hell 👹

While migrating an app to Expo, other developers would continue to work on it. The naive way to think about it would be to create a branch where the migration would happen. This would require that we carefully monitor the drift between this branch and the main one, and would be a time consuming and error prone effort.

We instead decided to create a new (temporary) project in our mono-repository that would load the entry point of our existing app.

Our setup schematically looked like this:

// our app root component
// <repo_root>/apps/app-1/AppRootComponent.tsx
export const AppRootComponent = () => {
return <OurWholeApp />
}

// entry point of the React Native JavaScript bundle where AppRootComponent is mounted
// <repo_root>/apps/app-1/index.js
import { AppRegistry } from "react-native";
import { AppRootComponent } from "./AppRootComponent";

AppRegistry.registerComponent('Our App', () => AppRootComponent);

// entry point of the Expo JavaScript bundle where AppRootComponent is mounted
// <repo_root>/apps/app-1-expo/index.js
import { registerRootComponent } from "expo";
import { AppRootComponent } from "../app-1/AppRootComponent";

registerRootComponent(AppRootComponent);

That way we could operate the migration within a dedicated folder without worrying about breaking the original app where new features would continue to be shipped every week.

Once the migration to Expo done, we started to ship the Expo version instead of the React Native one and kept the dual setup for a few weeks to fix the remaining rough patches.

We finally moved our code from <repo_root>/apps/app-1 to <repo_root>/apps/app-1-expo and then renamed the latter to <repo_root>/apps/app-1.

Identify and classify native code 🔍

Our oldest React Native app is ~7 years old, an eternity in the React Native world. The native code evolved during all these years to fit the React Native ecosystem changes and our sometimes very specific needs.

As a result, we had to carefully list, examine and classify native code (be it our own or third parties’):

  • Native code that did not require changes in our own native code (think about Automatic Linking)
  • Native code that required some specific setup in our native code (think about changes in AndroidManifest.xml, AppDelegate.mm or any other)

Once identified, we had to classify it. While the first category wouldn’t cause any problem during the migration, the second category would require further analysis to ensure that we could make it compatible with Expo.

This led to new sub categories with distinct action plan:

  • The native code provides an expo-config-plugin so we can add it to our expo-dev-client, then use it
  • If not, but an alternative of the native code provides an expo-config-plugin, assess if it’s worth changing to this alternative
  • If none of the above scenarios is possible, then develop our own plugins

That way, we ended up carefully crafting around 30 in-house Expo config plugins. It took us roughly 6 months to migrate our 4 apps to Expo — not only crafting these plugins and ensuring non breaking changes, but also adapting our continuous integration.

Expo and its impact on our CI 🔄

Now that we are able to produce a fully customized expo-dev-client for our needs, the objective is to distribute it to our 90 engineers.

Expo does provides such services through their Expo Application Services and I would strongly encourage you to rely on it. But in our case, with a longstanding history (some complex and specific release processes), we decided to continue to maintain it on our own.

So we adapted our CI workflows to first generate the native code of our Expo apps (think about Expo Prebuild) and then relied on Fastlane as we were already doing to compile and submit to the stores.

We created a new CI workflow that proved to be a game changer for our engineering team: build and distribute our custom expo-dev-client to both AppCenter and Amazon S3.

We distribute to AppCenter so engineers could install the expo-dev-client on a real device (iOS and Android), and to Amazon S3 so they can download and install on simulators (iOS and Android).

We arrived at the end of our journey! 🏁

We created a simple cli that would automate the download and installation of the expo-dev-client on any engineer’s simulator with a single line of script: yarn workspace app-1 install-dev-client -p [ios|android]

They now rely on that expo-dev-client for mobile development as they would rely to their browser for web development.

The Impact of the Transition 🎯

The transition to Expo has been a game-changer for our mobile development process. The feedback from our engineers has been overwhelmingly positive, with many citing the improved developer experience as a major benefit.

Also onboarding new engineers on mobile development is now significantly easier as it only requires 2 commands:

  • install the expo-dev-client
  • run the JavaScript bundler

In numbers 🎲

We went from an average 7 minutes of local build time to less than 2 minutes on S3 download and install on a simulator.
Before the migration to Expo, we conducted a git investigation on how frequent a change would happen on the native code and that subsequentely would require to rebuild the app.
Over the last five years this number varied from 5 to 760 per month, with a mean around 200.
We are a team of 90 engineers.

20% of the engineers needing to rebuild the app locally 10% of the native changes gives you the following hours saved per month:

const localBuildDurationInMin = 7;
const localInstallFromS3 = 2;
const gainPerBuild = localBuildDurationInMin - localInstallFromS3;
const averageNeededBuilds = 200;
const averageLocalBuilds = averageNeededBuilds * 0.1;
const numberOfEngineers = 90;
const numberOfEngineersWorkingOnMobile = numberOfEngineers * 0.2;
const totalGainInMinutes = gainPerBuild * averageLocalBuilds * numberOfEngineersWorkingOnMobile;
console.log(totalGainInMinutes / 60);
// 30

We currently save our engineering team around 30 hours per month of building time 🎉

No more Xcode, Xcode signing, Android or Java version issues, we don’t miss you! 😌

--

--