React Native JSI (Javascript Interface) is the new layer that helps in communication between Javascript and Native Platforms easier and faster. It is the core element in re-architecture of React Native with Fabric UI Layer and Turbo Modules.
How is JSI different?
JSI removes the need for a bridge between Native(Java/ObjC) and Javascript code. It also removes the requirement to serialize/deserialize all the information as JSON for communication between the two worlds. JSI is opening doors to new possibilities by bringing closes the javascript and the native worlds. Based on my understanding I am going to help you understand more about the JSI interface based on my knowledge.
Javascript Interface which allows us to register methods with the Javascript runtime. These methods are available via the global object in the Javascript world.
The methods can be entirely written in C++ or they can be a way to communicate with Objective C code on iOS and Java code in Android.
Any native module that is currently using the traditional bridge for communication between Javascript and the native worlds can be converted to a JSI module by writing a simple layer in C++
On iOS writing this layer is simple because C++ can run directly in Objective C hence all the iOS frameworks and code is available to use directly.
On android however we have to go an extra mile to do this through JNI.
These methods can be fully synchronous which means using async/await is not mandatory.
Now we are going to create a simple JSI Module which will help us understand everything even better.
Setting up our JSI Module
Open terminal in the desired directory where you want to create your library and run the following:
It will ask you some questions.
The important part is to choose C++ for iOS and Android when it asks for Which languages you want to use?
This will setup a basic module for us that uses C++ code. However note that this is not a JSI module. We need to change some parts of the code on Android and iOS to make it a JSI module.
Navigate to the react-native-simple-jsi folder that was just created and delete the example folder then create a new example in its place.
npx react-native init example.
It will also resolve all the other dependencies.
Configuring on Android
Now let's configure our library for android.
Prerequisite for android: Have NDK installed. Preferred version is 21.xx. Install Cmake 3.10.2. You can install both of these from SDK Manager in Android Studio
Okay, let's make this consumable. We are linking all the different libraries that we need for our jsi module here. We are telling CMake(Compiler for C++) how to compile our code and what directories to look for dependencies.
cmake_minimum_required: The minimum version of CMake required to compile our library.
add_library: We are telling the compiler, which libraries to add.
cpp is the name of our library.
SHARED means we are using shared c++ .so instead of compiling one to reduce size of our library.
We are including different files that we will need for our code to run. As you see, we have added path for jsi.cpp here too.
include_directories: Here we are telling the compiler to search for include files.
The remaining set_target_properties, find_library and target_link_libraries can be used as they are. Remember to change cpp to your desirable library name here.
build.gradle
Specify the minimum version of CMake to use while compiling c++ code.
externalNativeBuild {
cmake {
path "./CMakeLists.txt"
version "3.8.0+"
}
}
Step 3: Installing JSI Bindings
Run yarn add ../ inside the example folder to add our library to the example project.
Open example/android folder in Android Studio and wait for gradle to complete building your project.
If everything went as planned you should now see this in the Sidebar in Android Studio.
SimpleJsiModule.java
From the Sidebar navigate to react-native-simple-jsi/android/java/com.reactnativesimplejsi/SimpleJsiModule.java and replace it with the following code:
packagecom.reactnativesimplejsi;importandroid.util.Log;importandroidx.annotation.NonNull;importcom.facebook.react.bridge.JavaScriptContextHolder;importcom.facebook.react.bridge.ReactApplicationContext;importcom.facebook.react.bridge.ReactContextBaseJavaModule;importcom.facebook.react.module.annotations.ReactModule;@ReactModule(name=SimpleJsiModule.NAME)publicclassSimpleJsiModuleextendsReactContextBaseJavaModule{publicstaticfinalStringNAME="SimpleJsi";static{try{// Used to load the 'native-lib' library on application startup.System.loadLibrary("cpp");}catch(Exceptionignored){}}publicSimpleJsiModule(ReactApplicationContextreactContext){super(reactContext);}@Override@NonNullpublicStringgetName(){returnNAME;}privatenativevoidnativeInstall(longjsi);publicvoidinstallLib(JavaScriptContextHolderreactContext){if(reactContext.get()!=0){this.nativeInstall(reactContext.get());}else{Log.e("SimpleJsiModule","JSI Runtime is not available in debug mode");}}}
As you see, there are no @ReactMethod etc here. Two things are happening in this class.
We are loading our c++ library using System.loadLibrary.
We have an installLib method here which is basically looking for javascript runtime memory reference. The get method basically returns a long value. This value is passed over to JNI where we will install our bindings.
But we have an error, the nativeInstall function is not present in JNI.
Just click on Create JNI function for nativeInstall in the tooltip that shows when you move cursor over the method.
Now if you open cpp-adapter.cpp file. You will see a Java_com_reactnativesimplejsi_SimpleJsiModule_nativeInstall function added.
SimpleJsiModulePackage.java
This file does not exist. You have to create this java class.
Create a new java class and name it SimpleJsiModulePackage.
In this class we are overriding the getJSIModules method and installing our jsi bindings.
At this point our module is registered and running. So we are getting the module from react context and then calling installLib function to install our library.
While we could do this directly in our native module when it loads, it would not be safe because it is possible that the runtime is not loaded when the native module is ready. This package gives us more control and makes sure that runtime is available when we call installLib.
To call this method and install library we have to modify our app's MainApplication.java.
....importcom.facebook.react.bridge.JSIModulePackage;importcom.reactnativesimplejsi.SimpleJsiModulePackage;publicclassMainApplicationextendsApplicationimplementsReactApplication{privatefinalReactNativeHostmReactNativeHost=newReactNativeHost(this){@OverridepublicbooleangetUseDeveloperSupport(){returnBuildConfig.DEBUG;}@OverrideprotectedList<ReactPackage>getPackages(){@SuppressWarnings("UnnecessaryLocalVariable")List<ReactPackage>packages=newPackageList(this).getPackages();// Packages that cannot be autolinked yet can be added manually here, for SimpleJsiExample:// packages.add(new MyReactNativePackage());returnpackages;}@OverrideprotectedJSIModulePackagegetJSIModulePackage(){returnnewSimpleJsiModulePackage();}@OverrideprotectedStringgetJSMainModuleName(){return"index";}};.....
We are importing JSIModulePackage
We are registering our SimpleJsiModulePackage as a JSI Module so that when JS Runtime loads, our jsi bindings are also installed. Inside our instance of ReactNativeHost we are overriding getJSIModulePackage method and returning an new instance of SimpleJsiModulePackage.
cpp-adapter.cpp
This is our Java Native Interface (JNI) adapter which allows for two way communication between java and native c++ code. We can call c++ code from java and java code from c++.
jobject: The java class from which the function is called.
long value of our runtime memory reference.
We are reinterpreting the runtime class with auto runtime = reinterpret_cast<jsi::Runtime *>(jsi); and then calling install(*runtime); to install our bindings.
Configuring on iOS
Configuration on iOS is easier than android and includes a few simple step.
Run pod install in example/ios and open example.xcworkspace in xcode.
SimpleJsi.mm
Navigate to Pods > Development Pods > react-native-simple-jsi > ios and open SimpleJsi.mm.
Replace it with following code:
#import "SimpleJsi.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTUtils.h>
#import <jsi/jsi.h>
#import "example.h"
@implementationSimpleJsi@synthesizebridge=_bridge;@synthesizemethodQueue=_methodQueue;RCT_EXPORT_MODULE()+(BOOL)requiresMainQueueSetup{returnYES;}-(void)setBridge:(RCTBridge*)bridge{_bridge=bridge;_setBridgeOnMainQueue=RCTIsMainQueue();[selfinstallLibrary];}-(void)installLibrary{RCTCxxBridge*cxxBridge=(RCTCxxBridge*)self.bridge;if(!cxxBridge.runtime){dispatch_after(dispatch_time(DISPATCH_TIME_NOW,0.001*NSEC_PER_SEC),dispatch_get_main_queue(),^{/**
When refreshing the app while debugging, the setBridge
method is called too soon. The runtime is not ready yet
quite often. We need to install library as soon as runtime
becomes available.
*/[selfinstallLibrary];});return;}example::install(*(facebook::jsi::Runtime*)cxxBridge.runtime);}@end
At the top we are synthesising the bridge and methodQueue.
We are telling React that our module requires setup on Main Queue.
We are getting an instance of bridge which we will use to get the runtime and install our jsi bindings. Inside it we are checking if bridge.runtime exists or not. If it does not, we are waiting for sometime and then trying again until the bridge.runtime becomes available.
At the top, you see that we have included jsi include files.
The using namespace facebook etc helps us not write facebook:: over and over.
install function takes one parameter and that is our JS runtime. Inside this function we are registering a method by name helloWorld which will return a hello world string when we call it from javascript code.
Function::createFromHostFunction is a method creates a function which, when invoked, calls C++ code.
jsiRuntime.global().setProperty is where we bind our function with the javascript runtime global object.
A Value can be undefined, null, boolean, number, symbol, string, or object.
Conclusion
JSI is a game changer for React Native and and it is transforming the way React Native works. Today we have learnt how to build a simple JSI module. In the next blog, I will explain how we can convert any native module to a JSI module using some simple steps.
The complete code of the library and example app can be found on Github.
If you use Async Storage in your React Native App, you should give react-native-mmkv-storage a try. The fastest storage library for react native built with JSI.
An ultra fast (0.0002s read/write), small & encrypted mobile key-value storage framework for React Native written in C++ using JSI
Install the library
npm install react-native-mmkv-storage
For expo bare workflow
expo prebuild
Get Started with Documentation
What it is
This library aims to provide a fast & reliable solution for you data storage needs in react-native apps. It uses MMKV by Tencent under the hood on Android and iOS both that is used by their WeChat app(more than 1 Billion users). Unlike other storage solutions for React Native, this library lets you store any kind of data type, in any number of database instances, with or without encryption in a very fast and efficient way. Read about it on this blog post I wrote on dev.to
Learn how to build your own module with JSI on my blog
0.9.0 Breaking change
Works only with react native 0.71.0 and above. If you are on older version of react native, keep using 0.8.x.