React Native in an iOS/Android app

As a web developer, the potential to use JavaScript for building iOS and Android apps has always excited me. My first encounter with such a framework was Apache Cordova, an open source project with contributions from individuals and corporations like Google, Microsoft, Adobe, IBM, Intel, etc. I soon became a committer and started building developer tools for it as a part of my previous job.
At my new job, I wanted to continue working on mobile frameworks and joined the React Native team. Before moving full time on React Native Open Source, I took a slight detour and worked on the company's flagship app, parts of which are written in React Native (RN) . This blog post is a brief account of my experiences with using RN in a large scale iOS/Android app.

Brownfield and Green Field Apps

The main app has many years worth of code in it, spanning multiple teams over many thousand commits. The features are built using an array of internal and open sourced frameworks. Moving the entire app to one framework would be a herculean task for an app of this scale. The teams pick the technology for specific features based on technical merit, developer workflow and team skill-set. Thus, RN is used alongside the other frameworks and UI components, APIs and developer tools are also shared widely.
This is what is typically called a “brownfield” app in the community, as opposed to “greenfield” apps that are bootstrapped using RN from scratch. Internally however, this distinction is fluid since all apps are viewed as iOS or Android apps, using preexisting engineering infrastructure to various degrees.

Developer workflow

Most developers using React Native to build features, use Nuclide with its excellent Flow support. Nuclide is also pretty well integrated with technologies across the stack and has support for debugging and inspecting elements. It also proves useful occasionally when using the iOS debugger for native code.
I have also found myself dabbling in IntelliJ and XCode when developing/bridging native components or tracking down bugs that span languages. To produce simple test cases for reproducing bugs, we also have a standalone “kitchen sink” app with all components.
One of the biggest differences between the open source setup and our workflow was the use of BUCK as a build system; primarily proving useful due to its cached artifacts in a big project, reducing the overall build times. Most common tasks like adding new screens, generating tests, etc. are automated.

Native Components and Modules

To the end user, the entire app is one cohesive experience and parts of the app using RN should be no different from the rest of the app. The entire app has well defined user interface guidelines and CSS based styling in RN makes its implementation easy. Bridging native UI components for controls that exist is preferred as it promotes style consistency and is such controls are also well tested. We do not use react-native link, but instead rely on the BUCK files to manage JS and native dependencies separately, adding dependencies directly into the native code.
Flow definitions are used to ensure correctness between the JS API and the Java/ObjC implementations. This is vital for over the air updates as it ensures that JavaScript does not call into API methods that do no exist. Many other companies using React Native also seem to have similar tooling, and it would be awesome to see some of these tools open sourced.

Navigation

Our iOS and Android apps have their own navigation system to push view controllers or start activities using Intents. The RN navigation module plugins into this system and exposes API methods that make switching between RN and native screens seamless.
  • Each “route” declares a unique name and defines parts of the navigation UI like the title bar, action buttons, etc.
  • This information is then used to generate a JSON file at build time.
  • The RN navigation module uses this file to open the correct screen without having to wait for JavaScript to load or show a “redirection” screen.
Though the actual screen is drawn asynchronously, the navigation UI transitions smoothly as all the information can be inferred from the JSON file. This mechanism also helps when loading a screen directly from deep linking or clicking on a push notification.

Loading Screens

As you may have deduced from above, the JavaScript VM may not load until a screen needs it. The navigation also does not wait for React Native screen to be ready before pushing it into the viewport. As a result, there may be a momentary blank screen before JavaScript can kick in to show loading shimmers.
To mitigate this, I experimented with adding a loading screen in native code, possibly generated at build time from RN routes. The two issues that I noticed include
  1. Duplicating A/B testing logic that happens at runtime to ensure that the loading screens have the same appearance as the fully-loaded screens.
  2. Adding more native code during bridge startup also seems to delay RN start. This is only a problem when a network fetch may be faster than the time to start up the bridge.
In most cases, network was slower, and in addition to native loading screens, we found we could pre-load JS modules eagerly and even render parts of the component tree to be ready for the next React reconciliation cycle.
Systrace also comes in very handy when trying to understand the performance semantics of such optimizations.

Data Fetch

Most of the RN data fetches happen over Relay. However, there are also cases where we would need to re-use data that are already fetched by native screens. Having a single data fetch strategy would be ideal but for now, we just have a one-off data pre-fetcher so that the network portion of the critical path could be handled earlier. This native pre-fetcher also lends itself into interesting cases where we could predictively fetch data or even load the RN bridge if we believe that there is a high possibility for the user to request it.

Conclusion

While I have tried to cover some of the areas of RN I touched in the main app, there are many more areas like A/B testing, gesture handling, etc that I could not go over. Overall, I had a good exposure to complex React Native hybrid app development in the last six months. I learnt that it is important to embrace the platform and build for it.

The React Native team has expertise in iOS, Android and JavaScript, and were very helpful in my journey. Coming in as a Web Developer turned Program Manager, I now find myself now working with iOS and Android native code. I realize the importance of continuous learning and stepping outside my comfort zone to learn from other platforms !
I look forward to my next set of projects where I am hoping to help the React Native community.

A/B Testing for React Native apps with CodePush

A/B Testing (or Split Testing) is a way to optimize user behavior by serving different web pages to a random set of visitors and measuring metrics that determine the relative efficacy of each user interface design.
A/B testing on Websites is simple since the changes can be deployed and reverted quickly. On the contrary, doing this on mobile applications may require explicitly defining the scope of the A/B Experiment before submitting the apps to App Stores. Iterating on the experiments based on incoming metrics may also be delayed by the App Store processes. Scoping down the experiments to audience segments can also get tricky and may also need to be premeditated.
Though React Native generates native iOS and Android apps, the underlying JavaScript Engine that drives the app can utilize technologies like CodePush to enable web-like A/B tests. This post describes a way to use CodePush and other services in Mobile Center to set up A/B tests just as easily as one would do for websites.

Requirements

The three main requirements and the corresponding solutions for running A/B tests for React Native are. 
  1. Setup A/B experiments that can be improved as we start collecting user behavior data. For this, we need the ability to update the app instantly. 
    • Solution: CodePush.
  2. "Push" the experiments to the user rather than wait for the user to requests. We would also want to control the audience to ensure that confounding factors are eliminated
    • Solution: Push Notifications, with the ability to define Audience segments
  3. Collect data about user activity on the app to declare a winner in the A/B tests. For a mobile app, the analytics solution should not simply stream data to the server, but collect it (even when the app is offline) and send them to the server in batches. 
    • Solution: Mobile App Analytics. 
While I am using Mobile Center as it is convenient to pick up all the services from one place, any other solution for Notifications or analytics would also work.

App Setup

To get started, the SDKs need to be installed on the React Native app using npm install react-native-code-push mobile-center-analytics mobile-center-push and added to the project using the react-native link command. The link command also asks for mobile center keys and code-push deployment keys. You may need additional setup like configuring Google Cloud Messaging or enabling APNs for integrating Push notifications with the React Native app.
We would also have to add a snippet to listen to push notifications that are sent from the server.

This would be the basic app that needs to be distributed to our users.

Starting the experiments

  1. Define an A/B experiment case. This can range from simple things like the color of a checkout button to complicated workflow differences.
  2. Make changes to the code to incorporate the experiment. It would also help to save this experiment in the corresponding branch in source control. 
  3. Track user behavior by adding the track method at the appropriate points in code. We may also want to add additional information with the tracking information that identifies the specific case for which data is collected.
  4. Next, we would also create one deployment per case and release a bundle update for each of the deployment. Even if the app uses CodePush, these updates will not yet be available on the apps since they apps use a Staging or a Production Deployment by default.
  5. We would use Push Notifications to let the apps know about the new deployment key and the snippet above that is set up to run on receiving a push notification would do the job. Create a new push notification to be sent to the user as per the tutorial.
  6. Specify the codepush deployment key corresponding to the experiment case as the custom data in the push notification with the key called "codePushDeploymentKey".
  7. Select the right audiences that would receive the push notification and have their app updated with the specific experiment case.  
As users start recieving the notification, codepush's sync method will start picking up the new bundle with the experiment. Once the user performs actions on the app, analytics would be able to send data back to the server for that use case.

Conclusion

While this may not be available as an end-to-end scenario in Mobile Center yet, we can already use the existing infrastructure to A/B test our React Native apps and deliver better apps. Some points to note.
  • As with CodePush, note that you will not be able to add experiments that require changing native code. 
  • App Stores explicitly forbid changing the intent and behavior of apps. Ensure that the A/B tests still stay within what is allowed by the App Stores.
  • Many complex A/B testing scenarios are usually a matrix of features and experiments simultaneously. While this post does not cover those cases, the methodology described above can be used to build such a complex system.