This second article in an expert content series explores React Native internals we need to understand deeper when stepping off the standard path
So, you’re a developer or technical lead tasked with integrating React Native into an existing native app? Our content series is just what you need to successfully navigate this journey.
In part I of our series, we discussed the planning stage: understanding the scale of the challenge, essentials that should be in place before you start and some viable strategies to carry out the task.
Hopefully your organisation has such a plan in place, and now you’re at the point of adding React Native to the existing Android app: something that looks deceptively simple from the documentation, and can be simple if you’re lucky, but can be extremely challenging if you are not.
When we replace the preconfigured React Native Android and iOS shell apps with something fundamentally different and as complex and idiosyncratic as an established app, we can no longer rely on the pieces in the React Native black box just clicking into place. And when there are problems, those problems may be unique to this app integration, meaning there may be no documentation or existing search results to fall back on.
We’ll be going off-road, and we may need to fit pieces of React Native to things they weren’t exactly designed to fit. It’ll therefore help to first look under the hood and take some time to understand what those pieces of the React Native black box actually are.
What is React Native, under the hood?
This core functionality is done in a selection of libraries, written by Meta/Facebook in C++, shipped as
.so libraries, and then built from source for each binary type your app supports. Here’s an overview of some of the elements of the React Native ‘black box’ that you may need to deal with directly:
- Hermes is React Native’s own purpose-built JS engine and the new default since React Native 0.70 for both iOS and Android (September 2022).
2: React-like render:
- Layout engine: Yoga translates React flexbox-like trees inspired by the CSS flex model into Android and iOS equivalent layout styles.
- Surface renderer: there are two options and it’s possible for an app to use one on Android and another on iOS:
Wait, you keep saying C++…
Seriously though, having core React Native libraries as binaries produced by C++ is good for efficiency and performance, and it’s exceptionally rare to need to debug C++ in React Native (unless you choose to, for writing highest-performance native “turbo-modules”).
When wiring in a native app, however, you might need to troubleshoot the steps where React Native’s own C++
.so libraries are built and loaded, so it helps to know what these are:
Handles C++ builds via the Android NDK, or “Native Development Kit” — yes, it’s yet another new context for the word “Native”.
When thinking from a React Native perspective, we think of Java and Kotlin as “native” Android code, but when we step inside the
/android directory, everything is “native Android” in this sense.
Within Android development “native” generally refers to binaries built from C++ by NDK: these are ‘even more native’ than Android’s native Java or Kotlin, because it’s pure machine code that bypasses the Java virtual machine.
All of this is normally automatically in the build, but since NDK is a distinct tool, we might need to ensure that NDK is present and the right version is used — more on that when we deep-dive Android builds in part III of our series.
Handles C++ out of the box. This is because Objective-C can run C++ directly, so iOS’s Pods dependency management system can ship or import C++ without needing extra steps or tooling.
More on React Native’s New Architecture
Another thing you may have noticed from the list of React Native internals is that (as of 2023) React Native is in a transition period, moving from “Old Architecture” to “New Architecture”. There’s a lot written about the New Architecture, but the key theme is as follows:
- New Architecture shifts as much as possible to raw binary and endeavours to enable direct, synchronous communication with native APIs where possible.
At the time of writing (React Native 0.72), there are essentially two “New Architecture” switches that you’ll need to choose how to flip:
The Fabric rendering engine (off by default)
It also enables TurboModules. This is also desirable as it gives you more options and potentially much better performance for how you can connect your app’s legacy native features with React Native.
However, since it is not yet the default, it comes with the caveat that (as of 2023) some parts of the React Native ecosystem have not yet fully supported it, or their support is work-in-progress. A notable example is React Native Reanimated, which supports the new architecture in versions 3.x, but (at the time of writing) this branch is quite new, with some stability issues and challenges with wiring it into a non-standard app (compared to the older but more established 2.x branch).
This balance will of course change: Fabric will become the default, support for it will improve and stabilise (and modules that don’t will be supplanted by newer and better rivals), and eventually support for the Old Architecture will be dropped.
If your required modules support Fabric already, and you can get the build configuration to work, taking the plunge is advisable; but using the Old Architecture in the short term might be a necessary compromise.
These switches are platform-specific
Another thing to keep in mind is that these switches are platform-specific. If necessary, you could have Old Architecture on Android and New Architecture on iOS, or vice versa, or mix and match Hermes and Fabric between platforms. Consistency is obviously desirable, if possible, but mixed configurations are valid and may help if exceptionally difficult build conflicts are encountered.
iOS’s build toolchain is conceptually pretty simple. XCode (either the desktop application, CLI version, or XCode Cloud) handles the main build, and CocoaPods handles the dependencies. As with many things by Apple, it is purpose-built and often Just Works™, but sometimes Just Fails™ with opaque messages (especially where anything diverges from the happy path, or, after tooling updates).
React Native follows this pretty conventionally, with the only major difference being its autolinking system that pulls in pods from
Android uses Gradle for builds, which is a multi-purpose language-agnostic build system used in many projects and languages beyond Android and Java. Android’s Gradle setup is therefore more of a patchwork, with configuration spread across many files, using many plugins and delegated build tools (like NDK).
Gradle manages the whole build, but there are more independent moving parts under the hood, more places your native app may have diverged from standards presumed by React Native. It means that there more potential pitfalls.
Making it work
Each native app is very different, so wiring them in will have very different needs. The next parts of this series will look at Android and iOS in turn:
- Part 3 will focus on Android, arguably the more challenging due to the greater number of moving parts in its build process.
- Part 4 will focus on iOS, conceptually simpler but sometimes harder to debug, being designed more closely around a conventional “happy path”
Does your organisation want to cut its time to market, lower its maintenance costs and improve its product reliability? Incorporating React Native into your organisation delivers these outcomes and more. If you’d like to adopt React Native, get in touch. We’d love to discuss how we can help.