Integrating React Native with Babbel’s native mobile apps

The journey to contribute to mobile apps being an only-web team.
A girl using a phone

Back in mid-2018, my team discontinued a project to focus on our core product: the Babbel applications on Web and mobile. This change might sound easy. However, we wanted to support all Babbel users, but most of them were using our mobile apps and we were a web-only team. ReactJS was our frontend stack, and we had zero experience with native mobile development and their main languages (Kotlin and Swift).

We started an investigation phase to find the best approach for us to contribute to the mobile apps. With our lack of experience in native, we mainly looked for a technology that could integrate new features into an existing native app, while others can still be built and maintained natively.

The most promising candidate for us was React Native, a popular open-source framework introduced by Facebook in 2015 to enable efficient cross-platform mobile development. We also considered learning native mobile languages and other options in the market like Flutter or Webviews. In order to make a transparent decision, we created an Architectural Decision Record to collect pros and cons of every option and feedback from peers, especially colleagues who were native developers. One of the reasons we rejected Flutter was that learning new technologies would have stopped us from producing any work for an extended period of time. In addition, we believed any integration with Webview would have been cumbersome and might eventually come at the price of performance. In the end, we chose React Native for performance and productivity reasons.

React Native’s approach, “learn once, write anywhere” fitted our needs. Becoming a team with cross-platform end-to-end ownership, while bringing our web stack knowledge to mobile platforms, was a very promising outlook.

Other companies’ experiences

During the research phase, we carefully read the article written by Airbnb about their experience with React Native.

Airbnb mentioned in the article multiple downsides that didn’t apply to Babbel’s setup or the latest React Native’s status.

React Native has evolved and is now much more mature than in 2018. This includes improvements like the addition of Hermes (the new JavaScript engine), 64-bit support, and a new JavaScriptCore. Performance has also improved drastically.

After considering other companies’ key successes and failures like Udacity and multiple discussions with our teams, we created a proof of concept. It helped us to verify our assumptions, assess the complexity and get some buy-in from the native developers, who were still skeptics of the technology. After these tasks were completed, we were ready to go.

Looking for the least intrusive solution

Our first premise to start contributing to two established mobile repositories was to be the least intrusive as possible. These two native mono repositories were shared with several native mobile teams. Our intention was to minimize the impact on their workflows. The official way to integrate React Native components into a native application starts by introducing Node to the mobile project. An even bigger downside was the need to merge our Android and iOS repositories, and move the entire native projects into the respective folder (android or ios).

As we were not sure about the end of this adventure, we preferred to avoid any change in our process or our Continuous Integration flows.

Despite an apparent lack of information on the Internet, we eventually found another alternative: Electrode Native. This tool, developed by Walmart Labs, packages the React Native application into Containers which are versioned Android Archive (AAR) libraries for Android or frameworks for iOS.

Like any other native library, the React Native app could now be injected as a dependency into the native projects.

Dependencies.gradle file on Android

dependencies {
   implementation 'com.github.lessonnine:react-native-app:1.0.0'

Podfile on iOS

pod 'LearnerTools',
   :git => '',
   :tag => '1.0.0'

You might have deduced from the injection example that we use a single artifact for all our React Native features. While Electrode provides the concept of Mini-app, a way to encapsulate multiple apps, we decided to simplify the approach by using a single mini-app in order to share logic between them easily. For this, we provide a prop to the React Native instance which is used by the React Router to show the right component.

Below, you can see our current React Native features, the “Can-do Placement Test” (left), and the learning activity (right). Both inflate the React Native view as a Fragment in an Android Activity and as a UIViewController in iOS. This way allows us to integrate the components occupying the entire screen (left) or as a widget with other native components (right). For a better understanding, the React Native components are framed within a green area.

Setup and learning curve

The general idea is that working on a standalone React Native app prevents you from having to deal with native concepts like lifecycles, threads… however, this is not the case when the app is hybrid (Native plus React Native).

Our first contact with the native side was the presentation layer. For instance, on our Android app, we used fragments or activities to handle the UI. As React Native had to be rendered on one of these elements, we needed to learn how and when they were instantiated. In order to start a React Native component, we need to gather data from the app state before passing it to the component. As we were following the Clean Architecture we needed to work with the domain layers and its repositories. For these cases, we needed some base native language knowledge.

Independent of the technology we used, our components were part of a broader app, and they needed to communicate with the native side for essential things such as navigation. For this purpose, we introduced the bridge. This was a layer implemented on both sides, which allowed communication between native and React Native via events or requests.

We used the bridge for a couple of scenarios like navigating to a native screen after pressing a button in React Native, reporting an error so it can be tracked by the native Crashlytics implementation or store data in the native app state.

Dependencies locked by Electrode

Our React Native story has become quite coupled with Electrode Native, as we have a strong dependency on it to build our artifacts.

Electrode Native made it possible for us to inject our React Native code into native in an easy way. In the beginning, it kept us away from touching so much native code at a time when we were still not familiar with it. While it made the process quite smooth allowing us to proceed with such integration, it did come with a few disadvantages, too.

It was an extra layer with its own way to declare dependencies, which unfortunately blocked us from updating important dependencies like react-native in the past. Although it did not happen often, we can highlight here one of the dependency indirections we faced.

At Babbel, we use Lottie to create animations and share them between all our platforms. This library was not supported by Electrode Native, so we had to open a pull request to fix it. In addition, we could not use the same Lottie version as our native apps. At the time, Electrode Native was blocking us from updating react-native to version 0.60, and this version was required by Lottie. Hence, we were forced to downgrade our native Lottie dependency and change some of our animations across the entire app as they were not working anymore with an older version.

Nevertheless, we would still recommend Electrode Native for a non-invasive integration. The documentation was great and the development team were super responsive to our questions and pull requests.

Offline support and moving business logic to native

The first feature we implemented in React Native, the can-do placement test, did not need much communication with the native side. We only needed an attribute to be sent to a backend service. Developing the next mobile feature sounded easy for us. At this point, we did not foresee the mobile constraints and the challenges ahead.

One of the main differences between our mobile and web apps is offline support. Babbel offers a rich learning experience, allowing users to learn a language wherever and whenever they are. This is a challenge for our mobile platforms, as the user should get the best experience possible when mobile data is poor or not available.

For this to happen the app needs to store content in advance, to cache API calls in order to synchronize with the backend as soon as the user is online again and apply safety nets to cover all edge cases.

Initially, our tracking events were fired from React Native, making it fragile given a poor connection. React Native’s execution code was tied to its view being displayed, therefore if the view was detached, any retry or fallback strategy was not possible. Fortunately for us, all these functionalities were part of our native apps already, so we refactored our code by communicating through the bridge with the native implementation.

This solution applied later on for API calls too. We did not want to reinvent the wheel around authentication or tokens refresh. Hence we let the native app handle network requests by communicating the endpoint and payload to the native side and waited comfortably to receive the response.

Hiring new engineers is hard

After our first feature was implemented, the team wanted to increase its capacity in order to deliver faster and keep collaborating with the rest of the native teams. As we saw, due to the big dependency we had with the native side, an experienced React Native developer might not be the right fit for us. We looked for the unicorn person for a while, a native developer with some experience in JS and interest to work with React Native. This was unfortunately pretty rare, as React Native was still pretty new in the market and not many native developers were looking to switch stacks. After considering our current team strengths, we decided to give more weight to the native knowledge, as we were especially struggling in this area. Eventually, we found a suitable candidate, a native developer with interest in javascript and react native.

As part of our hiring process, our candidates had to complete a coding challenge to assess as best as possible the expected requirements. We adapted our original native coding challenge to best reflect the setup the candidate would later be working in. The challenge focused on creating the UI on React Native with a bit of logic, while the API calls and main business logic was located in native. In order to reduce the coding time for our candidates a basic bridge was already implemented.

And finally, where do we stand today?

You might be wondering what the outcome of this is. Are we happy? Do we regret it?

After one year and a half since we incorporated React Native to our stack, these are the results:

The good:

  • As a Web team, we were able to bring value to our mobile apps. We shipped a few features that otherwise might still not exist today, as the mobile teams had other priorities. Additionally, the can-do placement test was one of the most impactful features in 2019.
  • The vast majority of the React Native code is shared between iOS and Android.
  • Better developer experience compared to regular native development with instant page reloads.
  • Opens up to a mixed stack development, giving the autonomy to each team to choose whatever tech stack best suits their needs.
  • Bigger posibilities to build a feature on mobile and web at the same time reusing bigger portions of code.

The bad:

  • As we moved a lot of business logic to native, we ended up doing more native work than expected.
  • Lots of context and 3 repositories to work on made us slower.
  • Any global UI change (a button redesign for instance) affects now 3 repositories and not only 2.
  • We had a long setup phase due to our hybrid approach and lack of native knowledge.

The ugly:

  • It’s a risk that our team is the only one at Babbel having knowledge about React Native. Once the ownership of a feature changes or people move on to new jobs, things might get rewritten.
  • When a feature has a heavy dependency on logic from native, React Native might be only used for the UI layer. Otherwise, most of it can be created in React Native.
  • The testing process is slower than native. We need to create the artifact, inject it into the native app and create the final artifact for our QA team. The process can be automated although it requires some work to achieve that.

Although there are a lot of pain points, we remain positive as we have overcome the main infrastructure work. While we might consider implementing features with certain native work fully on native, we still see the potential for fully React Native features. Standalone features like learning tools or simple games will benefit from it and allow us to move faster.

Photo by Yura Fresh on Unsplash

Want to join our Engineering team?
Apply today!