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.
Contents
- 1 How does React Native’s original architecture work?
- 2 What is bridging in React Native?
- 3 How does React Native’s new architecture work?
- 4 What is JavaScript Interface (JSI)?
- 5 Understanding Codegen and TurboModules
- 6 Launch a React Native app using the new architecture
- 7 Create a device name TurboModule
- 8 Set folder structure
- 9 Set up package.json file
- 10 Set up podspec file
- 11 Define TypeScript interface for Codegen
- 12 Generate native iOS code
- 13 Implement module methods for iOS
- 14 Generate Android code
- 15 DeviceName.tsxCreate files for our React Native code
- 16 Create a unit converter TurboModule
- 17 Set up our folders and files
- 18 Define our TypeScript interface and unit conversion methods
- 19 Generate platform-specific code
- 20 Install, troubleshoot and use our modules
- 21 Summarize
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 ios
and android
folders, 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, Codegen
there 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-C
modules 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.properties
File and Settings newArchEnabled=true
. To enable it on iOS, open Terminal to Demo/ios
and 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 start
or . yarn start
In 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 JSISample
the folder, we need to create a new folder with the prefix RTN , for example RTNDeviceName
. Within this folder we will create three additional folders: ios
, android
and js
. We will also add two files next to the folder: package.json
and 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.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 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 podspec
the 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.
podspec
The 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.ts
new 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 Spec
interface, we define the methods we want to create – in this case, getDeviceName()
. Finally, we TurboModuleRegistry
call 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 RTNDeviceName
is 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 RTNDeviceName
modules folder . folder contains the native iOS code for our module. The folder structure should look similar to this:generated
generated
//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/ios
create two files in the folder: RTNDeviceName.h
and 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.h
and 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/android
to 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 ReactPackage
class. 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.java
file called , which will contain the actual implementation of our device name module on Android. It uses getDeviceName
the 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.tsx
Create files for our React Native code
Open src
Folders and create a component
folder. Inside component
the 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 started by importing several components and StyleSheet
modules from React Native. We also imported the React library and two Hooks. The last import statement is for RTNDeviceName
the 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 return
the declaration, we use a TouchableOpacity
component that calls a callback when pressed getDeviceName
. We also have a Text
component to render deviceName
the 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.tsx
render 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 RTNDeviceName
Demo folders. This time, we name the folder RTNConverter
.
Then, create the js
, ios
and android
folders, and the package.json
and rtn-converter.podspec
files. Our folder structure should look similar to RTNDeviceName
the initial folder structure of .
Next, package.json
add 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 podspec
the 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/js
creating 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 RTNDeviceName
very 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 JSISample
run 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 RTNConverter
folder.
Next, we’ll RTNConverter/ios
create 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-C
code to RTNConverter.mm
the 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.gradle
adding build.gradle
files 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.java
to add ReactPackage
the 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.java
groups the classes in the file.
Next, we will ConverterModule.java
add 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.tsx
import 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.