As a cross-platform mobile app development framework, React Native needs to communicate with platform-specific programming languages such as Java for Android and Objective-C for iOS. This can happen in one of two ways based on which architecture you use to construct hybrid mobile applications.
In React Native’s original or classic architecture, this communication process happens over what is known as the bridge. Meanwhile, the newer, more experimental architecture uses JavaScript Interface (JSI) to directly call methods that are implemented in Java or Objective-C.
Let’s go over how each option works at a high level and then explore using React Native JSI to improve our app’s speed and performance. You can check out the code demos used throughout this article in this GitHub repository.
As a note, this is a fairly advanced topic, so some experience with React Native programming is necessary to follow along. In addition, at least some familiarity with the basics of both Java and Objective-C will help you get the most out of this tutorial.
In React Native’s classic architecture, an application is divided into three distinct parts or threads:
Essentially, a thread is a sequence of executable instructions within a process. Threads are usually handled by a part of the operating system known as a scheduler, which contains instructions regarding when and how threads are executed.
To communicate and work together, these three threads rely on a mechanism known as the bridge. This mode of operation is always asynchronous and ensures that apps built with React Native will always be rendered using a platform-specific view and not a web view.
Due to this architecture, the JavaScript and Platform UI threads don’t communicate directly. As a result, a native method can’t be called directly in the JavaScript thread.
Generally, to develop an app that runs on either iOS or Android, we expect to use that platform‘s specific programming language. This is why a newly created React Native app comes with separate ios
and android
folders that serve as entry points to bootstrap our application for each respective platform.
Therefore, to run our JavaScript code on each platform, we rely on a framework known as JavaScriptCore. This means when we start a React Native app, we have to spin the three threads simultaneously while allowing communication to be handled by the bridge:
The bridge, which is written in C++, is how we send encoded messages formatted as JSON strings between the JavaScript and Platform UI threads:
As a result, some time is spent decoding the JSON string on each thread. Interestingly, the bridge also exposes an interface for Java or Objective-C to schedule JavaScript execution from native modules. It does all of this asynchronously. That time adds up!
Despite this, the bridge has generally worked well over the years, powering many applications. However, it has also had some other issues. For example:
The React Native team introduced the new architecture as part of their efforts to reduce these performance bottlenecks caused by the bridge. Let’s explore how.
React Native’s new architecture facilitates more direct communication between the JavaScript and Platform UI threads in comparison to the classic architecture. This means that native modules can be called directly in the JavaScript thread.
Some other differences in the new architecture include:
React Native apps using the new architecture will record a performance gain — including faster start times — because of these new implementations.
JavaScript Interface (JSI) fixes two critical shortcomings of the bridge:
Besides these great advantages, we can also use JSI to tap into a device’s connectivity features, such as Bluetooth and geolocation, by exposing methods that we can call directly with JavaScript.
The ability to invoke methods that are part of a platform’s native module is not completely new — we use the same pattern in web development. For example, in JavaScript, we can call DOM methods like so:
const paragraph = document.createElement('p')
We can even call methods on the created DOM. For example, this code calls a setHeight
method in C++, which changes the height of the element we created:
paragraph.setAttribute('height', 55)
As we can see, JSI — which is written in C++ — brings many improvements to the performance of our application. Next, let’s explore how to tap into its full potential using TurboModules and Codegen.
TurboModules are a special kind of native module in the new React Native architecture. Some of their benefits include:
Meanwhile, Codegen is like a static type checker and generator for our TurboModules. Essentially, when we define our types using TypeScript or Flow, we can use Codegen to generate C++ types for JSI. Codegen also generates more native code for our modules.
Generally, using Codegen and TurboModules allows us to use JSI to build modules that can communicate with platform-specific code like Java and Objective-C. It’s the recommended way to enjoy the benefits of JSI:
Now that we’ve covered this information at a high level, let’s put it into action. In the next section, we’ll create a TurboModule that will allow us access methods in either Java or Objective-C.
To create a new React Native app with the new architecture enabled, we first have to set up our folders. Let’s start by creating a folder — we’ll name ours JSISample
— where we’ll add our React Native app, device-name module, and the unit converter module.
For the next step, we can follow the setup instructions in the experimental React Native docs or simply open a new terminal and run the following command:
// Terminal npx react-native@latest init Demo
The command above will create a new React Native app that has Demo
as the folder name.
Once it has been installed successfully, we can enable the new architecture. To enable it for Android, simply open the Demo/android/gradle.properties
file and simply set newArchEnabled=true
. To enable it for iOS, open the terminal to Demo/ios
and run this command:
bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
Now the new React Native architecture should be enabled. To confirm this, you can start your app by running npm start
or yarn start
for iOS or Android. In the terminal, you should see the following:
LOG Running "Demo" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
With our app set up, let’s proceed to create two TurboModules: one to retrieve a device’s name and another for converting units.
To explore the importance of JSI, we will create a brand new TurboModule that we can install into a React Native application with the new architecture enabled.
The module we’ll create will enable us to retrieve a device’s name in our React Native app. This is useful for things like personalizing the user’s experience, distinguishing between different devices, and more.
In the JSISample
folder, we have to create a new folder with a prefix of RTN
, such as RTNDeviceName
. Inside this folder, we will create three additional folders: ios
, android
, and js
. We will also add two files alongside the folders: package.json
and rtn-device-name.podspec
.
At the moment, our folder structure should look like this:
// Folder structure RTNDeviceName ┣ android ┣ ios ┣ js ┣ package.json ┗ rtn-device-name.podspec
package.json
fileAs a React Native developer, you’ve certainly worked with package.json
files before. In the context of the new React Native architecture, this file both manages our module’s JavaScript code and interfaces with the platform-specific code we’ll set up later.
In the package.json
file, paste this code:
// RTNDeviceName/package.json { "name": "rtn-device-name", "version": "0.0.1", "description": "Convert units", "react-native": "js/index", "source": "js/index", "files": [ "js", "android", "ios", "rtn-device-name.podspec", "!android/build", "!ios/build", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__" ], "keywords": [ "react-native", "ios", "android" ], "repository": "https://github.com/bonarhyme/rtn-device-name", "author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)", "license": "MIT", "bugs": { "url": "https://github.com/bonarhyme/rtn-device-name/issues" }, "homepage": "https://github.com/bonarhyme/rtn-device-name#readme", "devDependencies": {}, "peerDependencies": { "react": "*", "react-native": "*" }, "codegenConfig": { "name": "RTNDeviceNameSpec", "type": "modules", "jsSrcsDir": "js", "android": { "javaPackageName": "com.rtndevicename" } } }
I provided my own details in the file above. Your file should look slightly different since you should use your personal, repository, and module details instead.
podspec
fileThe next file we’ll work on is the podspec
file, which is specifically for the iOS implementation of our demo app. Essentially, the podspec
file defines how the module we’re setting up interacts with the iOS build system as well as CocoaPods, the dependency manager for iOS apps.
You should see many similarities to the package.json
file we set up in the previous section, since we’re using values from that file to populate many of the fields in this file. Linking these two files ensures consistency across our JavaScript and native iOS code.
The podspec
file’s contents should look similar to this:
// RTNDeviceName/rtn-device-name.podspec require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "rtn-device-name" s.version = package["version"] s.summary = package["description"] s.description = package["description"] s.homepage = package["homepage"] s.license = package["license"] s.platforms = { :ios => "11.0" } s.author = package["author"] s.source = { :git => package["repository"], :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" install_modules_dependencies(s) end
Next on our to-do list is defining the TypeScript interface for Codegen. When setting up the file for this step, we must always use the following naming convention:
Native
Native
with our module name in PascalCaseFollowing this naming convention is critical to making React Native JSI work correctly. In our case, we’ll create a new file named NativeDeviceName.ts
and write the following code in it:
// RTNDeviceName/js/NativeDeviceName.ts import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { getDeviceName(): Promise<string>; } export default TurboModuleRegistry.get<Spec>('RTNDeviceName') as Spec | null;
This TypeScript file holds an interface for the methods that we will implement in this module. We start by importing the necessary React Native dependencies. Then, we define a Spec
interface, which extends a TurboModule
.
In our Spec
interface, we define the methods we want to create — in this case, getDeviceName()
. Finally, we call and export the module from TurboModuleRegistry
, specifying our Spec
interface and our module name.
We can return to this file later to update, add, or remove modules as needed.
In this step, we will use Codegen to generate native iOS code written in Objective-C. We simply have to ensure our terminal is open in the RTNDeviceName
folder and paste the following code:
// Terminal node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \ --path Demo/ \ --outputPath RTNDeviceName/generated/
This command generates a folder named generated
inside our RTNDeviceName
module folder. The generated
folder contain the native iOS code for our module. The folder structure should look similar to this:
// RTNDeviceName/generated/ generated ┗ build ┃ ┗ generated ┃ ┃ ┗ ios ┃ ┃ ┃ ┣ FBReactNativeSpec ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm ┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h ┃ ┃ ┃ ┣ RTNConverterSpec ┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm ┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h ┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp ┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h ┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp ┃ ┃ ┃ ┗ RTNConverterSpecJSI.h
In this step, we’ll have to write some iOS native code in Objective-C. The first step is to create two files inside our RTNDeviceName/ios
folder: RTNDeviceName.h
and RTNDeviceName.mm
.
The first file is a header file, a type of file used to hold functions that you can import into an Objective C file. Hence, we need to add this code to it:
// RTNDevicename/ios/RTNDeviceName.h #import <RTNDeviceNameSpec/RTNDeviceNameSpec.h> NS_ASSUME_NONNULL_BEGIN @interface RTNDeviceName : NSObject <NativeDeviceNameSpec> @end NS_ASSUME_NONNULL_END
The second file is an implementation file that holds the actual native code for our module. Add the following code:
// RTNDevicename/ios/RTNDeviceName.mm #import "RTNDeviceNameSpec.h" #import "RTNDeviceName.h" #import <UIKit/UIKit.h> @implementation RTNDeviceName RCT_EXPORT_MODULE() - (void)getDeviceName:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSString *deviceName = [UIDevice currentDevice].name; resolve(deviceName); } - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared<facebook::react::NativeDeviceNameSpecJSI>(params); } @end
In this file, we import the necessary header files, including the RTNDeviceName.h
we just created and UIKit, which we will use to extract the device’s name. Then we write the actual Objective-C function to extract and return the device name before writing the necessary boilerplate code below it.
Note that this boilerplate code was created by the React Native team. It helps us with the implementation of JSI since writing pure JSI means writing pure unadulterated native code.
The first step in the process of generating native Android code is to create a build.gradle
file inside the RTNDeviceName/android
folder. Then, add the following code to it:
// RTNDeviceName/android/build.gradle buildscript { ext.safeExtGet = {prop, fallback -> rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } repositories { google() gradlePluginPortal() } dependencies { classpath("com.android.tools.build:gradle:7.3.1") } } apply plugin: 'com.android.library' apply plugin: 'com.facebook.react' android { compileSdkVersion safeExtGet('compileSdkVersion', 33) namespace "com.rtndevicename" } repositories { mavenCentral() google() } dependencies { implementation 'com.facebook.react:react-native' }
Next, we will create our ReactPackage
class. Create a new file DeviceNamePackage.java
inside this deeply nested folder:
RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java
Inside the DeviceNamePackage.java
file we just created, add the following code, which groups related classes in Java:
// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java package com.rtndevicename; import androidx.annotation.Nullable; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.TurboReactPackage; import java.util.Collections; import java.util.List; import java.util.HashMap; import java.util.Map; public class DeviceNamePackage extends TurboReactPackage { @Nullable @Override public NativeModule getModule(String name, ReactApplicationContext reactContext) { if (name.equals(DeviceNameModule.NAME)) { return new DeviceNameModule(reactContext); } else { return null; } } @Override public ReactModuleInfoProvider getReactModuleInfoProvider() { return () -> { final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>(); moduleInfos.put( DeviceNameModule.NAME, new ReactModuleInfo( DeviceNameModule.NAME, DeviceNameModule.NAME, false, // canOverrideExistingModule false, // needsEagerInit true, // hasConstants false, // isCxxModule true // isTurboModule )); return moduleInfos; }; } }
We will also create a file named DeviceNameModule.java
that will contain the actual implementation of our device-name module on Android. It retrieves the device name using the getDeviceName
method. The rest of the file contains the boilerplate code needed for the code to properly interact with JSI.
Here’s the complete code for this file:
// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNameModule.java package com.rtndevicename; import androidx.annotation.NonNull; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import java.util.Map; import java.util.HashMap; import com.rtndevicename.NativeDeviceNameSpec; import android.os.Build; public class DeviceNameModule extends NativeDeviceNameSpec { public static String NAME = "RTNDeviceName"; DeviceNameModule(ReactApplicationContext context) { super(context); } @Override @NonNull public String getName() { return NAME; } @Override public void getDeviceName(Promise promise) { promise.resolve(Build.MODEL); } }
Having followed these steps, we can reinstall our module into our Demo
app by opening our terminal and running this command:
cd Demo yarn add ../RTNDeviceName
Also, we need to invoke Codegen to generate our Android code for our Demo
app like so:
cd android ./gradlew generateCodegenArtifactsFromSchema
This completes the process of creating a TurboModule. Our file structure should now look like this:
// Turbo module supposed structure RTNDeviceName ┣ android ┃ ┣ src ┃ ┃ ┗ main ┃ ┃ ┃ ┗ java ┃ ┃ ┃ ┃ ┗ com ┃ ┃ ┃ ┃ ┃ ┗ rtndevicename ┃ ┃ ┃ ┃ ┃ ┃ ┣ DeviceNameModule.java ┃ ┃ ┃ ┃ ┃ ┃ ┗ DeviceNamePackage.java ┃ ┗ build.gradle ┣ generated ┃ ┗ build ┃ ┃ ┗ generated ┃ ┃ ┃ ┗ ios ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec ┃ ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm ┃ ┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h ┃ ┃ ┃ ┃ ┣ RTNConverterSpec ┃ ┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm ┃ ┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h ┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp ┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h ┃ ┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp ┃ ┃ ┃ ┃ ┗ RTNConverterSpecJSI.h ┣ ios ┃ ┣ RTNDeviceName.h ┃ ┗ RTNDeviceName.mm ┣ js ┃ ┗ NativeDeviceName.ts ┣ package.json ┗ rtn-device-name.podspec
However, to use the RTNDeviceName
module we just created, we will have to set up our React Native code. Let’s see how to do that next.
DeviceName.tsx
file for our React Native codeOpen the src
folder and create a component
folder. Inside the component
folder, create a DeviceName.tsx
file and add the following code:
// Demo/src/components/DeviceName.tsx import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import React, {useCallback, useState} from 'react'; import RTNDeviceName from 'rtn-device-name/js/NativeDeviceName';
We begin by importing several components and the StyleSheet
module from React Native. We also import the React library and two Hooks. The last import
statement is for the RTNDeviceName
module we set up previously so we can access the native device-name-fetching functionality in our app.
Now, let’s break down the rest of the code we want to add to our file. First, we define a DeviceName
functional component. Inside our component, we create a state to hold the device name when we eventually fetch it:
export const DeviceName = () => { const [deviceName, setDeviceName] = useState<string | undefined>(''); // next code below here }
Next, we add an asynchronous callback. Inside it, we await
the response from the library and eventually set it to the state:
const getDeviceName = useCallback(async () => { const theDeviceName = await RTNDeviceName?.getDeviceName(); setDeviceName(theDeviceName); }, []); // Next piece of code below here
In the return
statement, we use a TouchableOpacity
component that calls the getDeviceName
callback on press. We also have a Text
component to render the deviceName
state:
return ( <View style={styles.container}> <TouchableOpacity onPress={getDeviceName} style={styles.button}> <Text style={styles.buttonText}>Get Device Name</Text> </TouchableOpacity> <Text style={styles.deviceName}>{deviceName}</Text> </View> );
At the end of the file, we define our component’s styles using the StyleSheet
module we imported earlier:
const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 10, justifyContent: 'center', alignItems: 'center', }, button: { justifyContent: 'center', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 10, backgroundColor: '#007bff', }, buttonText: { fontSize: 20, color: 'white', }, deviceName: { fontSize: 20, marginTop: 10, }, });
At this moment, we can call our component and render it in our src/App.tsx
file like so:
// Demo/App.tsx import React from 'react'; import {SafeAreaView, StatusBar, useColorScheme} from 'react-native'; import {Colors} from 'react-native/Libraries/NewAppScreen'; import {DeviceName} from './components/DeviceName'; function App(): JSX.Element { const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, flex: 1, }; return ( <SafeAreaView style={backgroundStyle}> <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={backgroundStyle.backgroundColor} /> <DeviceName /> </SafeAreaView> ); } export default App;
Finally, we can run our application using yarn start
or npm start
and follow the prompts to start it on iOS or Android. Our app should look similar to the below, depending on which device you start it on:
That’s it! We’ve successfully created a TurboModule that allows us to retrieve the user’s device name in our React Native app.
We’ve seen how easy it is to access a device’s information from our application directly from JavaScript. Now, we will build another module that will allow us to convert measurement units. We’ll follow similar setup steps, so we won’t go into as much detail about the code.
As before, we’ll start by creating a folder for our new TurboModule alongside the Demo
and RTNDeviceName
folders. This time, we’ll name this folder RTNConverter
.
Then, create the js
, ios
, and android
folders as well as the package.json
and rtn-converter.podspec
files. Our folder structure should look similar to the initial folder structure for RTNDeviceName
.
Next, add the appropriate code to the package.json
like so:
// RTNConverter/package.json { "name": "rtn-converter", "version": "0.0.1", "description": "Convert units", "react-native": "js/index", "source": "js/index", "files": [ "js", "android", "ios", "rtn-converter.podspec", "!android/build", "!ios/build", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__" ], "keywords": [ "react-native", "ios", "android" ], "repository": "https://github.com/bonarhyme/rtn-converter", "author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)", "license": "MIT", "bugs": { "url": "https://github.com/bonarhyme/rtn-converter/issues" }, "homepage": "https://github.com/bonarhyme/rtn-converter#readme", "devDependencies": {}, "peerDependencies": { "react": "*", "react-native": "*" }, "codegenConfig": { "name": "RTNConverterSpec", "type": "modules", "jsSrcsDir": "js", "android": { "javaPackageName": "com.rtnconverter" } } }
Remember to populate the fields with your own details. Then, add the podspec
file’s content:
// RTNConverter/podspec require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "rtn-converter" s.version = package["version"] s.summary = package["description"] s.description = package["description"] s.homepage = package["homepage"] s.license = package["license"] s.platforms = { :ios => "11.0" } s.author = package["author"] s.source = { :git => package["repository"], :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" install_modules_dependencies(s) end
Now, we’ll define the TypeScript interface for our unit converter by creating a NativeConverter.ts
file inside the RTNConverter/js
folder and adding the following code:
// RTNConverter/js/NativeConverter.ts import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { inchesToCentimeters(inches: number): Promise<number>; centimetersToInches(centimeters: number): Promise<number>; inchesToFeet(inches: number): Promise<number>; feetToInches(feet: number): Promise<number>; kilometersToMiles(kilometers: number): Promise<number>; milesToKilometers(miles: number): Promise<number>; feetToCentimeters(feet: number): Promise<number>; centimetersToFeet(centimeters: number): Promise<number>; yardsToMeters(yards: number): Promise<number>; metersToYards(meters: number): Promise<number>; milesToYards(miles: number): Promise<number>; yardsToMiles(yards: number): Promise<number>; feetToMeters(feet: number): Promise<number>; metersToFeet(meters: number): Promise<number>; } export default TurboModuleRegistry.get<Spec>('RTNConverter') as Spec | null;
As you can see, it contains the methods we will implement natively. It’s quite similar to what we did for the RTNDeviceName
module. However, instead of the getDeviceName()
method, we’ve defined several methods to convert between different units of measurement.
We’ll use a similar terminal command as before to generate iOS code. Open your terminal and run the command in the root of our JSISample
folder like so:
node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \ --path Demo/ \ --outputPath RTNConverter/generated/
Remember, this code uses Codegen to generate an iOS build inside our RTNConverter
folder.
Next, we’ll create the header and implementation files for our unit conversion module — RTNConverter.h
and RTNConverter.mm
— inside the RTNConverter/ios
folder. Inside the header file, add the following code:
// RTNConverter/ios/RTNConverter.h #import <RTNConverterSpec/RTNConverterSpec.h> NS_ASSUME_NONNULL_BEGIN @interface RTNConverter : NSObject <NativeConverterSpec> @end NS_ASSUME_NONNULL_END
Then, add the actual Objective-C code to the RTNConverter.mm
file:
// RTNConverter/ios/RTNConverter.mm #import "RTNConverterSpec.h" #import "RTNConverter.h" @implementation RTNConverter RCT_EXPORT_MODULE() - (void)inchesToCentimeters:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:inches*2.54]; resolve(result); } - (void)centimetersToInches:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/2.54]; resolve(result); } - (void)inchesToFeet:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:inches/12]; resolve(result); } - (void)feetToInches:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:feet*12]; resolve(result); } - (void)kilometersToMiles:(double)kilometers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:kilometers/1.609]; resolve(result); } - (void)milesToKilometers:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1.609]; resolve(result); } - (void)feetToCentimeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:feet*30.48]; resolve(result); } - (void)centimetersToFeet:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/30.48]; resolve(result); } - (void)yardsToMeters:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1.094]; resolve(result); } - (void)metersToYards:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:meters*1.094]; resolve(result); } - (void)milesToYards:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1760]; resolve(result); } - (void)yardsToMiles:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1760]; resolve(result); } - (void)feetToMeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:feet/3.281]; resolve(result); } - (void)metersToFeet:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NSNumber *result = [[NSNumber alloc] initWithDouble:meters*3.281]; resolve(result); } - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared<facebook::react::NativeConverterSpecJSI>(params); } @end
The implementation file for our device-name TurboModule contained a function to extract and return the device name. This time, it contains functions to calculate and return the converted measurements.
Now that we have the iOS code, it’s time to set up the Android code. Similar to before, we’ll start by adding the build.gradle
file inside the RTNConverter/android/build.gradle
:
// RTNConverter/android/build.gradle buildscript { ext.safeExtGet = {prop, fallback -> rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } repositories { google() gradlePluginPortal() } dependencies { classpath("com.android.tools.build:gradle:7.3.1") } } apply plugin: 'com.android.library' apply plugin: 'com.facebook.react' android { compileSdkVersion safeExtGet('compileSdkVersion', 33) namespace "com.rtnconverter" } repositories { mavenCentral() google() } dependencies { implementation 'com.facebook.react:react-native' }
Next, we will add the ReactPackage
class by creating a ConverterPackage.java
file inside this deeply nested folder:
RTNConverter/android/src/main/java/com/rtnconverter
Add the following code to this file:
// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java package com.rtnconverter; import androidx.annotation.Nullable; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.module.model.ReactModuleInfo; import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.TurboReactPackage; import java.util.Collections; import java.util.List; import java.util.HashMap; import java.util.Map; public class ConverterPackage extends TurboReactPackage { @Nullable @Override public NativeModule getModule(String name, ReactApplicationContext reactContext) { if (name.equals(ConverterModule.NAME)) { return new ConverterModule(reactContext); } else { return null; } } @Override public ReactModuleInfoProvider getReactModuleInfoProvider() { return () -> { final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>(); moduleInfos.put( ConverterModule.NAME, new ReactModuleInfo( ConverterModule.NAME, ConverterModule.NAME, false, // canOverrideExistingModule false, // needsEagerInit true, // hasConstants false, // isCxxModule true // isTurboModule )); return moduleInfos; }; } }
The code above groups classes together in Java. In our case, it groups the classes in the RTNConverterModule.java
file for us.
Next, we’ll add the actual implementation of our converter module for Android by creating a ConverterModule.java
file beside the file above. Then, add this code to the newly created file:
// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java package com.rtnconverter; import androidx.annotation.NonNull; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import java.util.Map; import java.util.HashMap; import com.rtnconverter.NativeConverterSpec; public class ConverterModule extends NativeConverterSpec { public static String NAME = "RTNConverter"; ConverterModule(ReactApplicationContext context) { super(context); } @Override @NonNull public String getName() { return NAME; } @Override public void inchesToCentimeters(double inches, Promise promise) { promise.resolve(inches * 2.54); } @Override public void centimetersToInches(double centimeters, Promise promise) { promise.resolve(centimeters / 2.54); } @Override public void inchesToFeet(double inches, Promise promise) { promise.resolve(inches / 12); } @Override public void feetToInches(double feet, Promise promise) { promise.resolve(feet * 12); } @Override public void kilometersToMiles(double kilometers, Promise promise) { promise.resolve(kilometers / 1.609); } @Override public void milesToKilometers(double miles, Promise promise) { promise.resolve(miles * 1.609); } @Override public void feetToCentimeters(double feet, Promise promise) { promise.resolve(feet * 30.48); } @Override public void centimetersToFeet(double centimeters, Promise promise) { promise.resolve(centimeters / 30.48); } @Override public void yardsToMeters(double yards, Promise promise) { promise.resolve(yards / 1.094); } @Override public void metersToYards(double meters, Promise promise) { promise.resolve(meters * 1.094); } @Override public void milesToYards(double miles, Promise promise) { promise.resolve(miles * 1760); } @Override public void yardsToMiles(double yards, Promise promise) { promise.resolve(yards / 1760); } @Override public void feetToMeters(double feet, Promise promise) { promise.resolve(feet / 3.281); } @Override public void metersToFeet(double meters, Promise promise) { promise.resolve(meters * 3.281); } }
Now that we have added the necessary native Android code for our module, we will then add it to our React Native app like so:
// Terminal cd Demo yarn add ../RTNConverter
Next, we will install our module for Android by running the following commands:
// Terminal cd android ./gradlew generateCodegenArtifactsFromSchema
Then, we will install our module for iOS by doing the following:
// Terminal cd ios RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
If you encounter any errors, you can clean the build and reinstall the module for our library like so:
cd ios rm -rf build bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
At the moment, our module is ready to be used in our React Native project. Let’s open our Demo/src/components
folder, create a UnitConverter.tsx
file, and add this code to it:
// Demo/src/components/UnitConverter.tsx import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import React, {useCallback, useState} from 'react'; import RTNCalculator from 'rtn-converter/js/NativeConverter'; const unitCombinationsConvertions = [ 'inchesToCentimeters', 'centimetersToInches', 'inchesToFeet', 'feetToInches', 'kilometersToMiles', 'milesToKilometers', 'feetToCentimeters', 'centimetersToFeet', 'yardsToMeters', 'metersToYards', 'milesToYards', 'yardsToMiles', 'feetToMeters', 'metersToFeet', ] as const; type UnitCombinationsConvertionsType = (typeof unitCombinationsConvertions)[number]; export const UnitConverter = () => { const [value, setValue] = useState<string>('0'); const [result, setResult] = useState<number | undefined>(); const [unitCombination, setUnitCombination] = useState< UnitCombinationsConvertionsType | undefined >(); const calculate = useCallback( async (combination: UnitCombinationsConvertionsType) => { const convertedValue = await RTNCalculator?.\[combination\](Number(value)); setUnitCombination(combination); setResult(convertedValue); }, [value], ); const camelCaseToWords = useCallback((word: string | undefined) => { if (!word) { return null; } const splitCamelCase = word.replace(/([A-Z])/g, ' $1'); return splitCamelCase.charAt(0).toUpperCase() + splitCamelCase.slice(1); }, []); return ( <View> <ScrollView contentInsetAdjustmentBehavior="automatic"> <View style={styles.container}> <Text style={styles.header}>JSI Unit Converter</Text> <View style={styles.computationContainer}> <View style={styles.calcContainer}> <TextInput value={value} onChangeText={e => setValue(e)} placeholder="Enter value" style={styles.textInput} inputMode="numeric" /> <Text style={styles.equalSign}>=</Text> <Text style={styles.result}>{result}</Text> </View> <Text style={styles.unitCombination}> {camelCaseToWords(unitCombination)} </Text> </View> <View style={styles.combinationContainer}> {unitCombinationsConvertions.map(combination => ( <TouchableOpacity key={combination} onPress={() => calculate(combination)} style={styles.combinationButton}> <Text style={styles.combinationButtonText}> {camelCaseToWords(combination)} </Text> </TouchableOpacity> ))} </View> </View> </ScrollView> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 10, }, header: { fontSize: 24, marginVertical: 20, textAlign: 'center', fontWeight: '700', }, computationContainer: { gap: 10, width: '90%', height: 100, marginTop: 10, }, calcContainer: { flexDirection: 'row', alignItems: 'center', gap: 12, height: 50, }, textInput: { borderWidth: 1, borderColor: 'gray', width: '50%', backgroundColor: 'lightgray', fontSize: 20, padding: 10, }, equalSign: { fontSize: 30, }, result: { width: '50%', height: 50, backgroundColor: 'gray', fontSize: 20, padding: 10, color: 'white', }, unitCombination: { fontSize: 16, }, combinationContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, justifyContent: 'center', }, combinationButton: { backgroundColor: 'gray', width: '45%', height: 30, justifyContent: 'center', paddingHorizontal: 5, }, combinationButtonText: { color: 'white', }, });
Then, we can import and use our component in the App.tsx
file like so:
// Demo/src/App.tsx import React from 'react'; import {SafeAreaView, StatusBar, useColorScheme} from 'react-native'; import {Colors} from 'react-native/Libraries/NewAppScreen'; import {UnitConverter} from './components/UnitConverter'; // import {DeviceName} from './components/DeviceName'; function App(): JSX.Element { const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, flex: 1, }; return ( <SafeAreaView style={backgroundStyle}> <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={backgroundStyle.backgroundColor} /> {/* <DeviceName /> */} <UnitConverter /> </SafeAreaView> ); } export default App;
The final app should look similar to the below:
React Native JSI is still an experimental feature. However, it looks promising in terms of enhancing our React Native application performance and development experience. It’s definitely worth a try!
In the sections above, we looked at the bridge in the classic React Native architecture and how it differs from the new architecture. We also explored the new architecture and how it can boost the speed and performance of our apps, covering concepts like JSI, Fabric, and TurboModules.
To get a better understanding of React Native JSI and the new architecture, we built a module to retrieve and display the user’s device’s name by accessing the native code directly. We also built a unit converter module that allows us to call the methods we defined in Java and Objective-C.
This guide should provide you with a great foundation to get started with JSI and the new React Native architecture. To review and play around with the code we implemented above, check out this GitHub repository. If you have any questions, you’re welcome to comment below.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]