React Native at Wix — The Architecture II (Deep dive)
By: Omri Bruchim
Tags: Software Architecture, Android, Multi Module, React Native, iOS

In my last post, I wrote about the high-level architecture of our Wix application. I explained how we adjust it to fit Wix organization structure (by using multi-module architecture, improving velocity, and shifting our environment to be Native agnostic), and how that helps us to achieve independent development and enhance the deployment experience for each group of developers.

Armed with this information, we can now dive deep into the code itself and see a practical implementation of such an architecture. We’ve previously outlined a few terms (Modules & Engine), which are used to describe the various pieces in the architecture. This post aims to illustrate some of them and points to relevant examples of code.

To better explain such a complex design, I chose to describe it from the bottom-up. First, we will jump into the Engine implementation, then build an example of a Module, and only afterwards review the main repository of the application.

For the sake of this example, I chose to build a simple blog application based on our react-native-crash-course by Dror Biran (recommended!). The example app will display a list of posts with the ability to view, add, and delete them.

The Engine

My example of an Engine implementation is called react-native-wix-engine , you can find the full source code here:

https://github.com/wix-incubator/react-native-wix-engine

It’s important to note that this is not the same Engine that’s used in the Wix application and it may not be the Engine you want to use — it’s just an example to help you better understand how to create your own.

To begin our journey, let’s create a new react-native project using react-native init .

Adding Native Libraries

As we discussed before, the Engine of your application should include all of the native implementation and the dependencies to 3rd party native libraries. Most likely things will differ from one application to another.

This is the point where you need to choose the libraries that will be the foundation of your application. When it comes to mobile app development using React Native, one of the most important steps is choosing the right navigation library for your project. Navigation serves as the backbone of an app and has a big impact on user experience. In my example, I chose react-native-navigation by Wix to achieve a fully native navigation experience. Another important library I decided to use is react-native-ui-lib — as a UI tool-set & components library.

You can add any libraries you want to the Engine, both JS and Native, and they will be available to be used by all the Modules in your application. If you install a Native library, remember to link it to native projects.

Pre-Built Binaries

The exciting part of the Engine is the pre-built binaries that are published to NPM together with the JS codebase of the Engine, and which are then used by Modules.

First, let’s see which files need to be built and published:

Android:

.APK file with debug symbols

file with debug symbols .APK file for release

iOS:

.APP file with debug symbols — built for Simulator (x86)

file with debug symbols — built for Simulator (x86) .APP file with debug symbols — built for a real iOS device (ARM)

file with debug symbols — built for a real iOS device (ARM) .APP file for release — built for a real iOS device (ARM)

In order to allow developers to debug applications with simulator\device in both release and debug mode, and to publish to the App Store and Google Play, we basically need to build our project 5 times. It means for every new Engine release, we should build all of these variations and publish them as an NPM package (here I chose to save the binaries in the same Engine NPM package, but it can be more efficient to save them in separate packages, and download only when required).

To build our Engine:

npm run build

You can review the full implementation of the publish script.

Remember that the pre-built binaries only include the native part, and we should also inject the JS bundle file, which brings us to the next section.

Engine Command Line Interface (CLI)

Among the goals we set was for our architecture and developers’ environment is to be native-agnostic.

Engine CLI is a command-line interface. The main purpose of our CLI is to replace react-native CLI (considering part of it is not relevant anymore) and to bypass the complexity of the Engine structure via a friendly interface for developers.

One example of Engine CLI usage is to install the pre-built on your device\simulator — which is a preferred and much faster option than using Xcode, Android Studio, or call react-native CLI command: react-native run-ios to build iOS\Android apps from scratch.

rn-wix-engine -run-ios \ rn-wix-engine -run-android — install the pre-built ios\android binary on all open simulators\devices, open the app, and run a local packager so the app fetches the bundle from it.

By using rn-wix-engine you can also:

Install\uninstall pre-built apps

Run the packager

Inject bundle file into pre-built release binaries (before publishing the app to store)

Other utils function from react-native CLI

You can basically add any command you want to the Engine CLI to improve the velocity of developers of Modules.

Project Structure

As you may notice, the structure of this project is a little bit different:

react-native-wix-engine

├── ...

├── inner_folder

│ └── react-native-wix-engine

│ ├── ios

│ ├── android

│ ├── src

│ ├── bin

│ ├── app_builds

│ └── ...

└── ...

The Engine holds the native projects (and binaries), and run locally from the library root folder, but at the same time it is also used by other projects as an NPM package, and so, the project structure can’t stay the same. One of the reasons for that is the location of the node_modules folder, which is installed by NPM in the root of the project which affect relative path in the code.

My Project

├── ...

├── node_modules

│ └── react-native-wix-engine

│ ├── ios

│ ├── android

│ ├── src

│ ├── bin

│ ├── app_builds

│ └── ...

└── package.json

For example, you can find the build.gradle file in your Android project which contains the relative path to the react-native package in your node_modules folder:

apply from: "../../node_modules/react-native/react.gradle"

Since we want to build the native project from both inside and outside the Engine repository, all relative paths to node_modules folder will fail, because from an external point of view, node_modules as a relative path moved two levels up, to be:

apply from: "../../../../node_modules/react-native/react.gradle"

We should simulate the same environment and restructure the Engine project structure. There are several ways to handle this challenge, I went with a simple trick and moved the Engine src folder codebase 2 levels inside (and add symlink to package.json to the root folder, for convenience)

Now, from either points of view: react-native-wix-engine root folder and any consumer of this library (installed in node_modules) the source code is the same.

To complete this restructure, we need to fix the relative path of node_modules in the native projects and remember to publish the Engine from the inner folder and not from the root folder.

Module Manager & Module Registry

Communication is basically the heart of your Engine — all traffic between Modules passes through the Engine, and that’s actually the best part here.

Module Manager is responsible for loading all the Modules, making sure their contract is valid, registering all Module APIs into the Module Registry, which holds all of the information about each Module and its contact.

Communication between Modules can be done in 4 ways: Method, Component, Broadcast, and Service. Later on, we’ll see an example of, and will talk more about each of them.

Running the Engine

If we run the Engine without a Module you should see this screen:

npm run start-empty-engine

Next, let’s make our app more interesting and implement a new module.