Woops, that got a little complex, didn’t it? And this is a simplified version… In order to understand this mess, we will describe its parts one by one, in the order in which they are created at launch. Let’s go!

Starting a React Native Application

Remember that the whole thing is built whenever a React Native application starts? Let us go through the multiple steps that separate the instant when you press your application’s logo on your phone from the moment everything becomes visible.

First, your application only has two things to work with:

The application’s code,

A unique thread, the main thread*, that is automatically assigned to it by the phone’s operating system.

To simplify explanations, we are going to conceptually split the code in two: the framework code — the one you do not have to write every time — and the custom code — that actually describes your application. Both being distributed over Javascript and native, this gives us a total of four parts of code to go through. The first thing that will get processed — on the main thread — is the native part of the framework code.

*also called UI Thread, as — outside of initialization — it will mainly be responsible of UI-related work

Creating the Native Foundations

One important thing to realize is that, although most of the UI code — <View> , <Text> … — is written in Javascript, what will in the end be rendered are only native views. That’s it. This means that the React Native framework needs to:

create native views and map their connections to Javascript components, store these natives views and display them.

While the first step will be handled by the UIManagerModule (which we will describe a little later), the RootView will take care of the second one. The RootView is more or less a container in which native views are organized in a big tree — a native representation of the Javascript component tree, if you like — and everything that will show up on the phone’s screen will be stored in there.

Back to our initializing process: everything starts with the creation of the above RootView — an empty container for now — before moving on to the Bridge Interface.

Wasn’t the bridge supposed to be between native and Javascript? Why would you need an interface there?

It is! But although most of the native side — including the RootView — is written in a platform-specific language (Objective-C or Java), the bridge is entirely implemented in C++ . The Bridge Interface therefore acts as an API, allowing the former to interact with the latter. The bridge itself is made up of two ends, native to Javascript, and vice versa.

The bridge, however, would be nothing without endpoints to dispatch calls to. These endpoints are Native Modules and will, in the end, be the only things available to the Javascript environment. In other words, everything but Native Modules will be eventually invisible to your Javascript application. For this reason, aside from the Custom Modules that you may or may not decide to create, the framework also includes Core Modules. One example of the latter would be the UIManagerModule, which stores a map of all Javascript UI Components and their associated native views. Every time a Javascript UI Component is created, updated or deleted, the UIManagerModule will use this map to accordingly create, update or delete the corresponding native view. It will also forward changes to the native view tree stored in the RootView in order to make them visible.

From the standpoint of initialization, all Native Modules are treated the same: for each module, an instance is created, and a reference to that instance is stored on the Javascript to native bridge — so that they can later be called from Javascript. In addition, a reference to the Bridge Interface is also passed to each Native Module, allowing them to call Javascript directly. Finally, two additional threads will be created: the JS Thread and the NativeModulesThread*.

*strictly speaking, it is not a unique thread but a pool of threads in the case of the iOS implementation of React Native.

Intermezzo: Setting Up the Javascript Engine

Before moving on, let’s make a quick summary of what has happened so far:

a bunch of native stuff has been created on the main thread,

we now have three threads to work with,

absolutely no Javascript has been processed yet.

Referring back to our initial map, what we have is this:

React Native’s native side

Meaning it is now time to load the Javascript bundle — framework and custom code alike!

Being an interpreted scripting language, Javascript cannot be run as is: it needs to be converted to bytecode, then be executed. This is the job of the Javascript virtual machine (a.k.a. Javascript engine). There are many Javascript engines out there, including Chrome’s V8, Mozilla’s SpiderMonkey and Safari’s JavaScriptCore… If in debug mode, React Native will use V8 and directly run in the browser, otherwise, it defaults to JavaScriptCore and runs on the device. As a side note, JavaScriptCore is not included in Android by default (while it is in iOS), so React Native automatically bundles a copy of it in the Android application — making Android applications slightly heavier than their iOS counterparts.

In any case, before effectively kicking off the Javascript engine, React Native has to give it a Context representing the execution environment. That includes the Javascript global object, and means that this global object is in fact created and stored on the C++ bridge. Why is that so important? Because the global object is then not only reachable from within the Javascript environment, but also from outside. It is therefore the primary means of communication between C++ (native) and Javascript, as it is through the global object that some native functions will be made available to Javascript — functions that will in turn be used to pass back data from Javascript to native.

Many things are stored on the global object, but the ModuleConfig array and the flushQueue() function are especially important. Each element of the ModuleConfig array describes a Native Module (be it Core or Custom), including its name, its exported constants, methods… The flushQueue() function plays a critical role in assuring communication between the Javascript and native environment, as it it will be used periodically to pass calls from the first back to the second.

Once the Javascript Context has been fully created and filled, it is fed to the Javascript engine that starts loading the React Native Javascript bundle on the JS Thread.

Loading the Javascript Bundle

As the virtual machine begins processing the Javascript part of the framework code, it will create the BatchedBridge. That name might ring a bell as it sometimes pops up in error messages! Despite its fancy denomination, it is but a simple queue, that stores “calls from Javascript to native”. A “call” is an object containing a native module ID, a method ID (for the specified native module), along with arguments that the native method is to be called with. Periodically (every 5 milliseconds by default), the BatchedBridge will call on global.flushQueue() , passing its content — an array of “calls” — to the Javascript to native end of the C++ bridge. Know as batches, these small arrays are indexed so as to ensure that all UI changes contained in one batch are made visible at the same time (this is necessary because the entire process is asynchronous). The Javascript to native end of the bridge will finally iterate over each call in a batch and dispatch them to the appropriate native module using the specified module ID — which it can do because it has a reference pointing to each and any native module, remember?

The next step is creating the NativeModules object — yes, the very object that has to be imported from ‘react-native’ each time you want to call a native module. The NativeModules object will be filled using the ModuleConfig array mentioned earlier. I will not go into the details of this process here, but it is roughly equivalent to doing NativeModules[module_name]={} for each module_name contained in the ModuleConfig , then NativeModules[module_name][method_name]=fillerMethod for each exported native method of the given module. fillerMethod is simply there to store all arguments it receives on the BatchedBridge , along with the method and module ID (something like fillerMethod = function(...args) { BatchedBridge.enqueueNativeCall(moduleID, methodID, args)} ), effectively creating a “call” from Javascript to native. That being said, what is fired when you later write MyNativeModule.myMethod(args) is actually the above fillerMethod !

We’re almost there. The last thing that needs to be done is creating the core JS Modules, among which the DeviceEventEmitter — that will be used to send events from native to Javascript — or the AppRegistry , that stores a reference to the main components of your app. In order to be callable from native, these modules are registered on the Javascript global object…

…and with that, the full React Native infrastructure has been built!

Making the React Native Application Visible

Despite the initialization being all but complete, our application is still invisible at this stage! Indeed, the loading of the Javascript bundle happened on the JS thread, which is independent of the main (a.k.a. UI) thread. The JS thread thus has to warn the main thread about the completion of its task, and in response, the main thread uses the AppRegistry (JS module) to ask the JS thread to process the main custom component — usually App.js .

To sum it up from a threading perspective, a React Native application’s launch process looks like this: