As a React Native developer, I appreciate the flexibility that comes from the framework's cross-platform functionality. But, there are moments when I do need access to native functionality. Enter Turbo Modules, React Native's magic door to the native world.
In this article I will start by covering what Turbo Modules are and the underlying need that led to their creation. From there, I will guide you through a step-by-step example where you'll learn how to create a custom Turbo Module and integrate it into your React Native app, enabling direct access to an Android Native API. This app can then be run on any Android or Fire OS device as shown in the demo below:
TLDR: Check out the Github repo here.
Why Turbo Modules matter - Overcoming the Limitations of the Native Modules
Previously when working with React Native, communication between the Native and JavaScript layers of applications was achieved through the JavaScript Bridge - also known as Native Modules. However, this approach had a number of drawbacks:
- The bridge operated asynchronously, meaning it would batch multiple calls to the native layer and invoke them at predetermined intervals.
- Data passing through the bridge had to undergo serialization and deserialization on the native side, introducing overhead and latency.
- The bridge lacked type safety. Any data could be passed across it without strict enforcement, leaving it up to the native layer to handle and process the data appropriately.
- During app startup, all native modules had to be loaded into memory, causing delays in launching the app for users.
To tackle these issues, the creators of React Native introduced:
Codegen, Turbo Modules and Fabric which form the New Architecture of React.
Turbo Modules are the next iteration of Native Modules that address the asynchronous and loading problems by lazy loading modules, allowing for faster app startup. Turbo Modules improve the performance of your app as by bypassing the JavaScript bridge and directly communicate with native code they reduce the overhead of communication between JavaScript and native code.
Codegen resolves the type safety concern by generating a JavaScript interface at build time. These interfaces ensure that the native code remains synchronized with the data passed from the JavaScript layer. Additionally, Codegen facilitates the creation of JSI bindings, which enable efficient and direct interaction between JavaScript and native code without the need for a bridge. Utilizing JSI bindings allows React Native applications to achieve faster and more optimized communication between the native and JavaScript layers.
In addition, Fabric is the new rendering system for React Native that leverages the capabilities of Turbo Modules and Codegen. Together, these three components form the pillars of the new architecture in React Native, providing enhanced performance, improved type safety, and streamlined interoperability between native and JavaScript code.
Phew, sounds complex! π€― So you might be asking yourself:
In what scenarioβs would I need a Turbo Module?
- Access to device APIs: Turbo Modules can grant you direct access to device APIs that are not exposed through standard JavaScript modules. This allows you to integrate with device-specific capabilities, such as accessing sensors, Bluetooth, or other hardware features. While in some cases you can use standard JavaScript modules, the performance might not be optimal, and you may not have access to all the advanced features provided by the native APIs. Turbo Modules create access to the full native functionality.
- Native UI components: Turbo Modules can be used to create custom native UI components that provide a more seamless and performant user experience compared to their JavaScript-based counterparts.
- CPU-intensive tasks: If your app performs CPU-intensive tasks, such as image processing, audio/video encoding, or complex calculations, using a Turbo Module can help offload these tasks to native code, taking advantage of the device's computational power and optimizing performance.
The steps to create a Turbo Module for your app
Letβs dive into how to add a custom Turbo Module to a React Native app that has the new architecture enabled. The example Turbo Module we will design will pull the model number of an Android device for our app to display on screen. We can then run our React Native app on any Android Device including Amazon Fire Devices.
Prerequisites
- React Native 0.69 - this is the version introduces the new Architecture.
- Typescript app - Codegen requires that we use types.
Tip: Remove any old versions of
react-native-cli package
, as it may cause unexpected build issues. You can use the commandnpm uninstall -g react-native-cli @react-native-community/cli
Step 1: Create a new app and setup Turbo Module folders
- Create a new folder called TurboModuleDemo and within it create a new app called DeviceName
npx react-native@latest init DeviceName
Tip: In order to keep the Turbo Module decoupled from the app, it's a good idea to define the module separately from the app and then later add it as a dependency to your app. This allows you to easily release it separately if needed.
- Within
TurboModuleDemo
, create a folder calledRTNDeviceName
. RTN stands for "React Native", and is a recommended prefix for React Native modules. - Within
RTNDeviceName
, create two subfolders:js
andandroid
.
Your folder structure should look like this:
TurboModulesDemo
βββ DeviceName
βββ RTNDeviceName
βββ android
βββ js
Step 2: JavaScript Specification
As mentioned, the New Architecture requires interfaces specified, so for this demo we will use TypeScript. Codegen will then use these specifications to generate code in strongly-typed languages ( C++, Objective-C++, Java)
- Within the
js
folder, create a file calledNativeGetDeviceName.ts
. Codegen will only look for files matching the pattern Native{MODULE_NAME} with a.ts
, or.tsx
extension. - Copy the following code into the file:
import type { TurboModule } from "react-native/Libraries/TurboModule/RCTExport";
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
getDeviceModel(): Promise<string>;
}
export default TurboModuleRegistry.get<Spec>("RTNDeviceName") as Spec | null;
Let's look into the code. First is the imports: the TurboModule type defines the base interface for all Turbo Modules and the TurboModuleRegistry JavaScript module contains functions for loading Turbo Modules.
The second section of the file contains the interface specification for the Turbo Module. In this case, the interface defines the getDeviceModel
function, which returns a promise that resolves to a string. This interface type must be named Spec for a Turbo Module.
Finally, we invoke TurboModuleRegistry.get
, passing the module's name, which will load the Turbo Native Module if it's available.
Step 3: Adding Configurations
Next, you will need to add some configuration to run Codegen. In the root of the RTNDeviceName
folder
- Add a
package.json
file with the following contents:
{
"name": "rtn-device",
"version": "0.0.1",
"description": "Get device name with Turbo Modules",
"react-native": "js/index",
"source": "js/index",
"files": [
"js",
"android",
"!android/build"
],
"keywords": [
"react-native",
"android"
],
"license": "MIT",
"devDependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "RTNDeviceSpec",
"type": "modules",
"jsSrcsDir": "js",
"android": {
"javaPackageName": "com.rtndevice"
}
}
}
Yarn will use this file when installing your module. It is also what contains the Codegen configuration - specified by the codegenConfig
field.
- Next, create a
build.gradle
file in the android folder, with the following contents:
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")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22")
}
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
apply plugin: 'org.jetbrains.kotlin.android'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
namespace "com.rtndevice"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}
This step creates a class, called DevicePackage
, that extends the TurboReactPackage
interface. This class serves as a bridge between the Turbo Module and the React Native app. Interestingly, you don't necessarily have to fully implement the package class. Even an empty implementation is sufficient for the app to recognize the Turbo Module as a React Native dependency and attempt to generate the necessary scaffolding code.
React Native relies on the DevicePackage
interface to determine which native classes should be used for the ViewManager and Native Modules exported by the library. By extending the TurboReactPackage
interface, you ensure that the Turbo Module is properly integrated into the React Native app's architecture.
This means that even if the package class appears to be empty or lacking implementation, the React Native app will still recognize and process the Turbo Module, attempting to generate the required code to make it functional.
- Create a folder called
rtndevice
under:android/src/main/java/com
. Inside the folder, create aDevicePackage.kt
file.
package com.rtndevice;
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReacTurboModuleoduleInfoProvider
class DevicePackage : TurboReactPackage() {
override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? = null
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider? = null
}
At the end of these steps, the android folder should look like this:
android
βββ build.gradle
βββ src
βββ main
βββ java
βββ com
βββ rtndevice
βββ DevicePackage.kt
Step 4: Adding Native Code
For the final step in creating your Turbo Module you'll need to write some native code to connect the JavaScript side to the native platforms. To generate the code for Android, you will need to invoke Codegen.
- From the
DeviceName
project folder run:
yarn add ../RTNDeviceName
cd android
./gradlew generateCodegenArtifactsFromSchema
Tip: You can verify the scaffolding code was generated by looking in:
DeviceName/node_modules/rtn-device/android/build/generated/source/codegen
Tip: Open the
android/gradle.properties
file within your app (DeviceName) and ensure thenewArchEnabled
property is true.
The native code for the Android side of a Turbo Module requires you to create a DeviceModule.kt that implements the module.
- In the
rtndevice
folder create aDeviceModule.kt
file:
android
βββ build.gradle
βββ src
βββ main
βββ java
βββ com
βββ rtndevice
βββ DeviceModule.kt
βββ DevicePackage.kt
- Add the following code the the
DeviceModule.kt
file:
package com.rtndevice
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.rtndevice.NativeGetDeviceNameSpec
import android.os.Build
class DeviceModule(reactContext: ReactApplicationContext) : NativeGetDeviceNameSpec(reactContext) {
override fun getName() = NAME
override fun getDeviceModel(promise: Promise) {
val manufacturer: String = Build.MANUFACTURER
val model: String = Build.MODEL
promise.resolve(manufacturer + model)
}
companion object {
const val NAME = "RTNDeviceName"
}
}
This class implements the DeviceModule
which extends the NativeGetDeviceNameSpec
interface that was generated by codegen from the NativeGetDeviceName TypeScript specification file. It is also the class that contains our getDeviceModel
function, that returns a promise with the device model as a string.
- Update the DevicePackage.kt
package com.rtndevice;
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.module.model.ReactModuleInfo
class DevicePackage : TurboReactPackage() {
override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? =
if (name == DeviceModule.NAME) {
DeviceModule(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
DeviceModule.NAME to ReactModuleInfo(
DeviceModule.NAME,
DeviceModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
)
}
}
Step 5: Adding the Turbo Module to your App
- To add the Turbo Module to your app, from your DeviceName app folder, re-run:
yarn add ../RTNDeviceName
Tip: To ensure the changes in your TurboModule are reflected in your app delete your
node_modules
before performing the yarn add.
Now you can use your Turbo Module to use the getDeviceName
function in your app!
- In your
App.tsx
call thegetDeviceModel
method:
import React from 'react';
import {useState} from 'react';
import {SafeAreaView, StatusBar, Text, Button} from 'react-native';
import RTNDeviceName from 'rtn-device/js/NativeGetDeviceName';
const App: () => JSX.Element = () => {
const [result, setResult] = useState<string | null>(null);
return (
<SafeAreaView>
<StatusBar barStyle={'dark-content'} />
<Text style={{marginLeft: 20, marginTop: 20}}>
{result ?? 'Whats my device?'}
</Text>
<Button
title="Compute"
onPress={async () => {
const value = await RTNDeviceName?.getDeviceModel();
setResult(value ?? null);
}}
/>
</SafeAreaView>
);
};
export default App;
Check out your Turbo Module in action by running npm run android
on any Android device including the Amazon Fire OS Devices:
Congratulations for successfully implementing a simple Turbo Module in an app! Hopefully with this article and the sample code you now have a better understanding for how to integrate Turbo Modules into your projects.
Let me know in the comments what you create a Turbo Module for!
π£ Follow me at anisha.dev
πΊ Subscribe to our AmazonAppstoreDevelopers Youtube channel
π§ Sign up for the Amazon Developer Newsletter