First announced in 2018, the React Native re-architecture is a massive effort Facebook has been undertaking to address some of the long-standing issues1.
"What do you dislike about React Native?" / "What do you dislike about React Native?" (June 2019 Edition)
Facebook team wants to rethink the bridge (async message approach) to overcome its limitations.
- JSI (JavaScript Interface) & JSC (JavaScriptCore)
- React Native Fabric (UI-Layer Re-architecture)
- TurboModules (NativeModules Re-architecture)
- CodeGen
- Lean Core
In order for JavaScript code to run in a native mobile application it should rely on an engine to be interpreted.
The JSI is not part of React Native per se — it is a unified, lightweight, general-purpose layer for (theoretically) any JavaScript engine.
NATIVE <--> JSI <--> JS ENGINE
Benefits:
- JSC could be swapped out more easily for other engines (e.g. ChakraCore by Microsoft and V8 by Google).
- The cornerstone of the whole re-architecture — is that by using JSI, JavaScript can hold reference to C++ Host Objects and invoke methods on them. => JavaScript and Native realms will be truly aware of each other, and there won’t be any need to serialize to JSON the messages to pass across, removing all congestion on
the bridge.
Fabric - the re-architecture of the UI manager that aims to modernize the rendering layer of React Native.
Fabric will allow for the UI manager to create the Shadow Tree directly in C++.
=> increases the swiftness of the process by reducing the number of “jumps” across realms.
=> improves the responsiveness of the UI.
By using the JSI, Fabric exposes the UI operations to JavaScript as functions => the new Shadow Tree (which determines what to really show on screen) is shared between the two realms, allowing straight interaction from both ends:
JAVASCRIPT <--> Shadow Tree in C++ <--> NATIVE
=> this direct control from the JavaScript side allows to have the priority queues from the new React 16 for the UI operations2, in order to have opt-in synchronous executions where it benefits performance.
=> improvements in common pitfalls like lists, navigation, and gesture handling.
In the current implementation, the Native Modules used by JavaScript code (e.g. Bluetooth) need to be initialized when the app is opened — even when they’re not used — because of the “unawareness” between the realms.
The new TurboModules approach allows the JavaScript code to load each module only when it’s really needed, and to hold direct reference to it.
=> no more need to communicate using batched JSON messages on the bridge.
=> improves startup time for applications with lots of Native Modules.
Interact with native code (ObjC/Java/Swift/Kotlin/Go/...) from React Native through JSI.
(How to expose native code to JavaScript without going through the bridge?)
- Create a C++ binding.
- Install the binding into the JS runtime.
- Call native method from JS.
1. Create a C++ binding.
The binding is a C++ class that implements the jsi::HostObject
interface.
#include "Test.h"
#include <jsi/jsi.h>
namespace facebook {
namespace react {
// Exposes Test class to JavaScript realm.
class TestBinding : public jsi::HostObject {
public:
// Installs TestBinding into JavaScript runtime.
static void install(jsi::Runtime &runtime, std::shared_ptr<TestBinding> testBinding);
TestBinding(std::unique_ptr<Test> test);
// `jsi::HostObject` specific overloads.
jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
private:
std::unique_ptr<Test> test_;
};
} // namespace react
} // namespace facebook
1. Create a C++ binding.
jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;
void TestBinding::install(jsi::Runtime &runtime,
std::shared_ptr<TestBinding> testBinding) {
// What is the name that JS will use when it reaches for this, e.g. `global.nativeTest`
auto testModuleName = "nativeTest";
// Create a JS object version of our binding
auto object = jsi::Object::createFromHostObject(runtime, testBinding);
// Set the "nativeTest" property
runtime.global().setProperty(runtime, testModuleName, std::move(object));
}
// Call from JS
global.nativeTest.foo
2. Install the binding into the JS runtime.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleJavaScriptDidLoadNotification:)
name:RCTJavaScriptDidLoadNotification object:bridge];
}
- (void)handleJavaScriptDidLoadNotification:(__unused NSNotification*)notification {
// Get the RCTCxxBridge from the bridge
RCTCxxBridge* bridge = notification.userInfo[@”bridge”];
// Get the runtime
facebook::jsi::Runtime* runtime = (facebook::jsi::Runtime*)bridge.runtime;
// Create the Test object
auto test = std::make_unique<facebook::react::Test>();
// Create the Test binding
std::shared_ptr<facebook::react::TestBinding> testBinding_ =
std::make_shared<facebook::react::TestBinding>(std::move(test));
// Install it!
facebook::react::TestBinding::install((*runtime), testBinding_);
}
3. Call native method from JS.
// Call from JS
global.nativeTest.foo
jsi::Value TestBinding::get(jsi::Runtime &runtime, const jsi::PropNameID &name) {
auto methodName = name.utf8(runtime);
auto &test = *test_;
if (methodName == "foo") {
return jsi::Function::createFromHostFunction(
runtime, name, 0, [&test]
(jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *arguments, size_t count)
-> jsi::Value {
return test.foo();
}
);
}
return jsi::Value::undefined();
}
The React Native team is also doubling down on the presence of a static type checker (either Flow or TypeScript) in the code.
In particular, they are working on a tool called CodeGen to "automate" the compatibility between JS and the native side. By using the typed JavaScript as the source of truth, CodeGen can generate the interface files needed by Fabric and TurboModules to send messages across the realms with confidence.
In both TurboModule and Fabric, interface available to JavaScript could be defined using Flow or TypeScript. This interface definition can be further leveraged to generate many of the C++ classes, and the interfaces/protocols for Java/ObjC implementations.
For example, in case of TurboModules, the C++ class that wraps the ObjC/Java class and exposes the methods using a JSI object can be generated. This will ensure that all JavaScript calls have implementations available on the native side, and will continue to ensure this with over the air updates like CodePush.
Typed JS -> [CodeGen] -> JSI Bindings/ObjC Protocols/Abstract Java Classes + Schemas -> Fabric/TurboModules
CodeGen automates creation of JSI C++ bindings and abstracts away C++ code from RN developers.
Benefits:
- Compile-time type safety!
- Safe over-the-air updates
Lean Core initiative has an aim to break down bloated React Native codebase into different repositories.
Benefits:
- Make the codebase more approachable
- Deprecate older modules
- Reduce the weight of a generated app => faster startup time
- Help the community move fast and enable PRs to be reviewed and merged quicker
Articles: The New React Native Architecture Explained. Part One: React and Codegen The New React Native Architecture Explained. Part Two: JSI and JSC The New React Native Architecture Explained. Part Three: Fabric and TurboModules The New React Native Architecture Explained. Part Four: Lean Core React Native's New Architecture - Glossary of Terms React Native JSI Challenge React Native JSI Challenge #2 Interacting with Go from React Native through JSI
Videos: React Native's New Architecture - Parashuram N - React Conf 2018 React Native EU 2019: Alexey Kureev - React Native CodeGen
Footnotes
- ↩
-
Concurrent React and the Scheduler / Suspense suspends a component rendering and renders a fallback component until a condition is met. ↩
PDF version: https://www.dropbox.com/s/jmemea0p3yigln5/RN-arch.pdf