Improve speed and performance with React Native JSI

First published on public account 

front-end hybrid development , welcome to pay attention.

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 be achieved in one of two ways, depending on the architecture you use to build your hybrid mobile app.

In the original or traditional architecture of React Native, this communication process is achieved through so-called bridging . Meanwhile, newer, more experimental architectures use the JavaScript Interface (JSI) to directly call methods implemented in Java or Objective-C.

Let’s take a high-level look at how each option works, then explore using React Native JSI to improve the speed and performance of our apps. You can view a demo of the code used in this article in this GitHub repository .

It’s important to note that this is a relatively advanced topic, so some React Native programming experience is required to keep up. Additionally, having at least some familiarity with Java and Objective-C basics will help you get the most from this tutorial.

How does React Native’s original architecture work?

In the traditional architecture of React Native, an application is divided into three different parts or threads:

  • JavaScript thread: where we write JavaScript code and business logic, and create nodes
  • Shadow tree: Where React Native uses the Yoga framework for layout calculations.
  • Platform UI (Java or Objective-C): Render this calculated layout.

Essentially, a thread is a series of executable instructions in a process. Threads are usually handled by a part of the operating system called the scheduler, which contains instructions on when and how to execute threads.

In order to communicate and work together, these three threads rely on a mechanism called bridging. This mode of operation is always asynchronous and ensures that apps built with React Native are always rendered using platform-specific views rather than web views.

Because of this architecture, the JavaScript and platform UI threads do not communicate directly. Therefore, native methods cannot be called directly from the JavaScript thread.

Typically, in order to develop an app that runs on iOS or Android, we expect to use that platform’s specific programming language. This is why newly created React Native apps have separate iosand androidfolders, which serve as entry points to guide our app to run on their respective platforms.

Therefore, in order to run our JavaScript code on every platform, we rely on a framework called JavaScriptCore. This means that when we start a React Native app, we have to start three threads simultaneously while allowing the bridge to handle the communication.

What is bridging in React Native?

Bridges , written in C++, are how we send encoded messages between JavaScript and the platform UI thread, formatted as JSON strings:

  • The JavaScript thread decides which node to render on the screen and uses the bridge to deliver the message in serialized JSON format.
  • Before the message reaches the main thread, it needs to reach the shadow tree thread, which calculates the position of the node on the screen.
  • However, the main thread handles actions that occur on the UI, such as typing in a text field or pressing a button.

Therefore, each thread takes some time to decode the JSON string. Interestingly, the bridge also provides an interface to Java or Objective-C for scheduling JavaScript execution from native modules. It does all this asynchronously. That adds up to a lot of time!

Nonetheless, bridges have generally worked well over the years, powering many applications. However, it also encountered some other problems. For example:

  • Bridges are closely tied to the lifecycle of React Native and are typically initialized or shut down when React Native is initialized or shut down, which means slower startup times.
  • If the communication process between threads is blocked in some way while the user is interacting with the user interface – for example, when scrolling through a long list of data, they may momentarily see a white blank space, resulting in a poor user experience
  • Every module that will be used in the application needs to be initialized at startup, which can lead to slower startup times, increased system resource usage, scalability issues, and more.

The React Native team introduced a new architecture in an effort to reduce performance bottlenecks caused by bridging. Let’s explore how to do this.

How does React Native’s new architecture work?

Compared with the classic architecture, React Native’s new architecture is more convenient for direct communication between JavaScript and the platform UI thread. This means native modules can be called directly from the JavaScript thread.

Some other differences in the new architecture include:

  • Ability to work with multiple general-purpose engines (such as Hermes or V8) instead of just relying on the JavaScriptCore engine
  • No need to serialize or deserialize messages between JavaScript and the platform UI thread. Instead, it uses a mechanism called the JavaScript Interface, or JSI, which we discuss in detail below.
  • Use Fabric instead of Yoga to render UI components
  • TurboModules were introduced to ensure that every module used in the application is only loaded when needed, not at startup.

As a result of these new implementations, React Native apps using the new architecture will record performance improvements—including faster startup times.

What is JavaScript Interface (JSI)?

The JavaScript Interface (JSI) fixes two key flaws of bridging:

  • Allows us to call native methods created in Java or Objective-C directly from JavaScript
  • Allows us to communicate synchronously or asynchronously with native code, which improves startup time and enables faster response in scenarios like scrolling long lists

In addition to these great advantages, we can also use JSI to take advantage of device connectivity features, such as Bluetooth and geolocation, by exposing methods that we can call directly from JavaScript.

The ability to call methods in modules native to the platform is not entirely new – we use the same pattern in web development. For example, in JavaScript we can call DOM methods like this:

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 written in C++ brings many performance improvements to our application. Next, let’s explore how to leverage TurboModules and Codegen to their full potential.

Understanding Codegen and TurboModules

TurboModules are a special kind of native module in the new React Native architecture. Some of their advantages include:

  • Initialize modules only when needed for faster application startup times
  • Using JSI for native code means smoother communication between the platform UI and JavaScript threads
  • Provide strongly typed interfaces on native platforms

At the same time, Codegenthere are static type checkers and generators like our TurboModules. Essentially, when we use TypeScript or Flow to define our types, we can use Codegen to generate C++ types for JSI. Codegen also generates more native code for our modules.

In general, using Codegen and TurboModules enables us to use JSI to build Objective-Cmodules that can communicate with platform-specific code such as Java and JSI. This is 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 practice. In the next section, we will create a TurboModule that will allow us to access methods in Java or Objective-C.

Launch a React Native app using the new architecture

In order to create a new React Native app with the new architecture enabled, we first need to set up our folders. Let’s start by creating a folder – we’ll name it JSISample – where we’ll add our React Native app, device name module, and unit converter module.

For the next step, we can follow the setup guide in the experimental React Native documentation, or simply open a new terminal and run the following command:

// Terminal 
npx react- native @latest init Demo

The above command will create a new React Native app with the folder name Demo.

Once installed successfully, we can enable the new architecture. To enable it on Android, just open Demo/android/gradle.propertiesFile and Settings newArchEnabled=true. To enable it on iOS, open Terminal to Demo/iosand run this command:

bundle install && RCT_NEW_ARCH_ENABLED= 1  bundle exec pod install

The new React Native architecture should now be enabled. To confirm this, you can launch your iOS or Android app by running npm startor . yarn startIn the terminal, you should see the following:

LOG   Running "Demo"  with { "fabric" : true , "initialProps" :{ "concurrentRoot" : true }, "rootTag" : 1 }

With our application set up, let’s go ahead and create two more TurboModules: one for getting the name of the device and another for unit conversion.

Create a device name TurboModule

To explore the importance of JSI, we will create a brand new TurboModule that we can install into our React Native application with the new architecture enabled.

Set folder structure

Within JSISamplethe folder, we need to create a new folder with the prefix RTN , for example RTNDeviceName. Within this folder we will create three additional folders: iosandroidand js. We will also add two files next to the folder: package.jsonand rtn-device-name.podspec.

Currently, our folder structure should look like this:

//Folder structure
RTNDeviceName
 ┣ android
 ┣ ios
 ┣js
 ┣ package .json
 ┗ rtn-device-name.podspec

Set up package.json file

As a React Native developer, you must have dealt with package.jsonfiles 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 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 have provided my details in the above document. Your file should look slightly different as you should use your own personal information, repository and module details.

Set up podspec file

The next file we’re going to work on is podspecthe file, which is specially prepared for the iOS implementation of our demo app. Essentially, the podspec file defines how the module we are setting up interacts with the iOS build system and CocoaPods (the dependency manager for iOS applications).

You should see a lot of similarities to the file we set up in the previous section package.json, as we use values ​​from that file to populate many of the fields in this file. Linking these two files ensures consistency between our JavaScript and native iOS code.

podspecThe contents of the file 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

Define TypeScript interface for Codegen

Next on our to-do list is defining Codegen’s TypeScript interface. When setting up files for this step, we must always use the following naming convention:

  • Start filename with Native
  • Follow Native with our module name, using PascalCase nomenclature

Following this naming convention is crucial to making React Native JSI work correctly. In our case, we will create a NativeDeviceName.tsnew file called 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 contains the interface for the methods we will implement in this module. We first import the necessary React Native dependencies. Then, we define a Spec interface, which extends a TurboModule.

In our Specinterface, we define the methods we want to create – in this case, getDeviceName(). Finally, we TurboModuleRegistrycall and export the module from , specifying our Spec interface and our module name.

Generate native iOS code

In this step, we will use Codegen to generate native iOS code written in Objective-C. We just need to make sure our terminal RTNDeviceNameis open in the folder and paste the following code:

// Terminal

node Demo /node_modules/ react- native /scripts/g enerate-codegen- artifacts .js \
  --path Demo/ \
  --outputPath RTNDeviceName /generated/

This command generates a folder named inside our RTNDeviceNamemodules folder . folder contains the native iOS code for our module. The folder structure should look similar to this:generatedgenerated

//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

Implement module methods for iOS

In this step, we need to write some iOS native code in Objective-C. First, we need to RTNDeviceName/ioscreate two files in the folder: RTNDeviceName.hand RTNDeviceName.mm.

The first file is a header file, which is used to store functions that can be imported into the Objective C file. So 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, which contains 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 headers, including the one we just created RTNDeviceName.hand UIKit, which we will use to extract the name of the device. We then write the actual Objective-C function to extract and return the device name, and then write the necessary boilerplate code underneath it.

Please note that this boilerplate code was created by the React Native team. It helps us implement JSI because writing pure JSI means writing pure, unadulterated native code.

Generate Android code

The first step in generating native Android code is RTNDeviceName/androidto create a file within the folder build.gradle. 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 ReactPackageclass. Create a new file within this deeply nested folder DeviceNamePackage.java.

RTNDeviceName /android/ src /main/ java /com/ rtndevicename/DeviceNamePackage.java

In the file we just created DeviceNamePackage.java, 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 DeviceNameModule.javafile called , which will contain the actual implementation of our device name module on Android. It uses getDeviceNamethe method to get the device name. The rest of the file contains the boilerplate code required for your code to interact correctly with JSI.

Here is the complete code of the 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);
    }
}

After following these steps, we can reinstall our module into our Demo app by opening a terminal and running this command:

cd Demo
 yarn add ../RTNDeviceName

In addition, we need to call Codegen to generate our Android code for our Demo application, as shown below:

cd android
./gradlew generateCodegenArtifactsFromSchema

This completes the process of creating 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 module we just created RTNDeviceName, we must set up our React Native code. Let’s see how to do it next.

DeviceName.tsxCreate files for our React Native code

Open srcFolders and create a componentfolder. Inside componentthe folder, create a DeviceName.tsxfile 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 started by importing several components and StyleSheetmodules from React Native. We also imported the React library and two Hooks. The last import statement is for RTNDeviceNamethe module we set up before so that we can access the native device name acquisition function in the application.

Now, let’s break down the rest of the code we want to add to the file. First, we define a DeviceName functional component. Inside our component we create a state to hold the device name we finally get:

export  const  DeviceName = () => {
   const [deviceName, setDeviceName] = useState< string | undefined >( '' );
 // next code below here 
}

Next, we add an asynchronous callback. In it, we wait for the response from the library and finally set it into the state:

  const getDeviceName = useCallback( async () => {
     const theDeviceName = await RTNDeviceName?.getDeviceName();
    setDeviceName(theDeviceName);
  }, []);

// Next piece of code below here

In returnthe declaration, we use a TouchableOpacitycomponent that calls a callback when pressed getDeviceName. We also have a Textcomponent to render deviceNamethe 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 use the StyleSheet module we imported earlier to define the styles for our component:

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 point, we can call our component and src/App.tsxrender it in our file like this:

// 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 app using yarn start or npm start and follow the prompts to launch it on iOS or Android. Our app should look similar to the following, depending on which device you launch it on:

That’s it! We have successfully created a TurboModule that allows us to get the user’s device name in a React Native app.

Create a unit converter TurboModule

We’ve seen how easy it is to access a device’s information directly from a JavaScript application. Now we will build another module that allows us to convert units of measurement. We’ll follow similar setup steps, so we won’t go into too much detail on the code.

Set up our folders and files

As before, we will first create a new TurboModule folder, alongside the Demo and RTNDeviceNameDemo folders. This time, we name the folder RTNConverter.

Then, create the jsiosand androidfolders, and the package.jsonand rtn-converter.podspecfiles. Our folder structure should look similar to RTNDeviceNamethe initial folder structure of .

Next, package.jsonadd the appropriate code to like this:

//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 fill in the fields with your own details. Then, add podspecthe contents of the file:

//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

Define our TypeScript interface and unit conversion methods

Now we will define the TypeScript interface of our unit converter by RTNConverter/jscreating a file in the folder and adding the following code:NativeConverter.ts

//RTNConverter/js/NativeConverter.ts

import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  inches ToCentimeters( inches : number ) : Promise<number>;
  centimeters ToInches( centimeters : number ) : Promise<number>;
  inches ToFeet( inches : number ) : Promise<number>;
  feet ToInches( feet : number ) : Promise<number>;
  kilometers ToMiles( kilometers : number ) : Promise<number>;
  miles ToKilometers( miles : number ) : Promise<number>;
  feet ToCentimeters( feet : number ) : Promise<number>;
  centimeters ToFeet( centimeters : number ) : Promise<number>;
  yards ToMeters( yards : number ) : Promise<number>;
  meters ToYards( meters : number ) : Promise<number>;
  miles ToYards( miles : number ) : Promise<number>;
  yards ToMiles( yards : number ) : Promise<number>;
  feet ToMeters( feet : number ) : Promise<number>;
  meters ToFeet( 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. This is RTNDeviceNamevery similar to what we did for the module. However, we have defined several methods to convert different measurement units instead of getDeviceName()methods.

Generate platform-specific code

We will use similar terminal commands as before to generate iOS code. Open your terminal and JSISamplerun the command in the root of our folder like this:

node Demo /node_modules/ react- native /scripts/g enerate-codegen- artifacts .js \
  --path Demo/ \
  --outputPath RTNConverter /generated/

Remember, this code uses Codegen to generate an iOS build inside our RTNConverterfolder.

Next, we’ll RTNConverter/ioscreate the header and implementation files for our unit conversion module inside the folder – RTNConverter.h and RTNConverter.mm. In 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-Ccode to RTNConverter.mmthe 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

Our device name TurboModule implementation file contains a function that extracts and returns the device name. This time, it contains functions that calculate and return the converted measurement.

Now that we have the iOS code, it’s time to set up the Android code. Similar to before, we’ll start RTNConverter/android/build.gradleadding build.gradlefiles in .

//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 create a file within this deeply nested folder ConverterPackage.javato add ReactPackagethe class

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 above code groups classes in Java. In our case, it RTNConverterModule.javagroups the classes in the file.

Next, we will ConverterModule.javaadd the actual implementation of our converter module for Android by creating a file that will sit next to the above file. 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 to our module, we will add it to our React Native app like this:

// Terminal
CD Demo
yarn add ../RTNConverter

Install, troubleshoot and use our modules

Next, we will install our module for Android by running the following command:

// Terminal

cd android
 ./gradlew generateCodegenArtifactsFromSchema

We will then 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 our library’s modules as follows:

cd ios
rm -rf build
 bundle install && RCT_NEW_ARCH_ENABLED= 1  bundle exec pod install

Currently, our module is ready to be used in our React Native project. Let’s open Demo/src/components the 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  UnitCombinationsConversionsType =
  ( typeof unitCombinationsConversions)[ 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 ( /([AZ])/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} >
            {unitCombinationsConversions.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' ,
  },
});

We can then App.tsximport and use our component in a file like this:

// 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 application should look similar to the following:

Summarize

React Native JSI is still an experimental feature. However, it looks very promising from the perspective of improving the performance and development experience of our React Native apps. It’s definitely worth a try!

In the above section, we looked at bridging in the classic React Native architecture and how it differs from newer architectures. We also explored the new architecture and how it can improve the speed and performance of our applications, covering concepts such as JSI, Fabric, and TurboModules.

To better understand React Native JSI and the new architecture, we built a module to retrieve and display the name of the user’s device by accessing the native code directly. We also built a unit conversion module that allows us to call methods we define in Java and Objective-C.