If you've worked with React Native, you may be familiar with the concept of Native Modules, which allow JavaScript and platform-native code to communicate over the React Native "bridge", which handles cross-platform serialization via JSON.
Turbo Native Modules are the next iteration on Native Modules that provide a few extra benefits:
- Strongly typed interfaces that are consistent across platforms
- The ability to write your code in C++, either exclusively or integrated with another native platform language, reducing the need to duplicate implementations across platforms
- Lazy loading of modules, allowing for faster app startup
- The use of JSI, a JavaScript interface for native code, allows for more efficient communication between native and JavaScript code than the bridge
At a high-level the steps for writing a Turbo Module are:
- Define a JavaScript specification using Flow or TypeScript.
- Configure the dependencies management system to generate code from the provided spec.
- Implement the Native code.
- Integrate the code in the app.
This guide will show you how to create a basic Turbo Native Module compatible with the latest version of React Native.
Note
You can also setup local library containing Turbo Native Module with one command. Read the guide to Local libraries setup for more details.
To create a Turbo Native Module, we need to:
- Define the JavaScript specification.
- Configure the module so that Codegen can generate the scaffolding.
- Write the native code to finish implementing the module.
In order to keep the module decoupled from the app, it's a good idea to define the module separately from the app and then add it as a dependency to your app later. This is also what you'll do for writing Turbo Native Modules that can be released as open-source libraries later.
Next to your application, create a folder called RTNCalculator
. RTN stands for "React Native", and is a recommended prefix for React Native modules.
Within RTNCalculator
, create three subfolders: js
, ios
, and android
.
The final result should look like this:
TurboModulesGuide
├── MyApp
└── RTNCalculator
├── android
├── ios
└── js
The New Architecture requires interfaces specified in a typed dialect of JavaScript (either Flow or TypeScript). Codegen will use these specifications to generate code in strongly-typed languages, including C++, Objective-C++, and Java.
There are two requirements the file containing this specification must meet:
- The file must be named
Native<MODULE_NAME>
, with a.js
or.jsx
extension when using Flow, or a.ts
, or.tsx
extension when using TypeScript. Codegen will only look for files matching this pattern. - The file must export a
TurboModuleRegistrySpec
object.
Flow
// @flow
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
add(a: number, b: number): Promise<number>;
}
export default (TurboModuleRegistry.get<Spec>(
'RTNCalculator'
): ?Spec);
TypeScript
import { TurboModule, TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
add(a: number, b: number): Promise<number>;
}
export default TurboModuleRegistry.get<Spec>("RTNCalculator") as Spec | null;
Place the file in the js
directory that we have just created, eg: js/NativeRTNCalculator.ts
.
At the beginning of the spec files are the imports:
- The
TurboModule
type, which defines the base interface for all Turbo Native Modules - The
TurboModuleRegistry
JavaScript module, which contains functions for loading Turbo Native Modules
The second section of the file contains the interface specification for the Turbo Native Module. In this case, the interface defines the add
function, which takes two numbers and returns a promise that resolves to a number. This interface type must be named Spec
for a Turbo Native Module.
Finally, we invoke TurboModuleRegistry.get
, passing the module's name, which will load the Turbo Native Module if it's available.
Warning
We are writing JavaScript files importing types from libraries, without setting up a proper node module and installing its dependencies. Your IDE will not be able to resolve the import statements and you may see errors and warnings. This is expected and will not cause problems when you add the module to your app.
Next, you need to add some configuration for Codegen and auto-linking.
Some configuration files are shared between iOS and Android, while the others are platform-specific.
The shared configuration is a package.json
file used by yarn when installing your module. Create the package.json
file in the root of the RTNCalculator
directory.
{
"name": "rtn-calculator",
"version": "0.0.1",
"description": "Add numbers with Turbo Native Modules",
"react-native": "js/index",
"source": "js/index",
"files": [
"js",
"android",
"ios",
"rtn-calculator.podspec",
"!android/build",
"!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
],
"keywords": ["react-native", "ios", "android"],
"repository": "https://github.com/<your_github_handle>/rtn-calculator",
"author": "<Your Name> <your_email@your_provider.com> (https://github.com/<your_github_handle>)",
"license": "MIT",
"bugs": {
"url": "https://github.com/<your_github_handle>/rtn-calculator/issues"
},
"homepage": "https://github.com/<your_github_handle>/rtn-calculator#readme",
"devDependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "RTNCalculatorSpec",
"type": "modules",
"jsSrcsDir": "js",
"android": {
"javaPackageName": "com.rtncalculator"
}
}
}
The upper part of the file contains some descriptive information like the name of the component, its version, and its source files. Make sure to update the various placeholders which are wrapped in <>
: replace all the occurrences of the <your_github_handle>
, <Your Name>
, and <your_email@your_provider.com>
tokens.
Then there are the dependencies for this package. For this guide, you need react
and react-native
.
Finally, the Codegen configuration is specified by the codegenConfig
field. It contains an object that defines the module through four fields:
name
: The name of the library. By convention, you should add theSpec
suffix.type
: The type of module contained by this package. In this case, it is a Turbo Native Module; thus, the value to use ismodules
.jsSrcsDir
: the relative path to access thejs
specification that is parsed by Codegen.android.javaPackageName
: the package to use in the Java files generated by Codegen.
For iOS, you'll need to create a rtn-calculator.podspec
file, which will define the module as a dependency for your app. It will stay in the root of RTNCalculator
, alongside the ios
folder.
The file will look like this:
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "rtn-calculator"
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
The .podspec
file has to be a sibling of the package.json
file, and its name is the one we set in the package.json
's name
property: rtn-calculator
.
The first part of the file prepares some variables that we use throughout the file. Then, there is a section that contains some information used to configure the pod, like its name, version, and description.
All the requirements for the New Architecture have been encapsulated in the install_modules_dependencies
. It takes care of installing the proper dependencies based on which architecture is currently enabled. It also automatically installs the React-Core
dependency in the old architecture.
To prepare Android to run Codegen you have to:
- Update the
build.gradle
file. - A Java/Kotlin class that implements the
ReactPackage
interface
At the end of these steps, the android
folder should look like this:
android
├── build.gradle
└── src
└── main
└── java
└── com
└── rtncalculator
└── CalculatorPackage.java
First, create a build.gradle
file in the android
folder, with the following contents:
Java
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.rtncalculator"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}
Kotlin
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.rtncalculator"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}
Then, you need a class that extends the BaseReactPackage
interface. To run the Codegen process, you don't have to completely implement the package class: an empty implementation is enough for the app to pick up the module as a proper React Native dependency and to try and generate the scaffolding code.
Create an android/src/main/java/com/rtncalculator
folder and, inside that folder, create a CalculatorPackage.java
file.
Java
package com.rtncalculator;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.BaseReactPackage;
import java.util.Collections;
import java.util.List;
public class CalculatorPackage extends BaseReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
return null;
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return null;
}
}
Kotlin
package com.rtncalculator;
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProvider
class CalculatorPackage : BaseReactPackage() {
override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? = null
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider? = null
}
React Native uses the ReactPackage
interface to understand what native classes the app has to use for the ViewManager
and Native Modules
exported by the library.
For the final step in getting your Turbo Native Module ready to go, you'll need to write some native code to connect the JavaScript side to the native platforms. This process requires two main steps:
- Run Codegen to see what it generates.
- Write your native code, implementing the generated interfaces.
When developing a React Native app that uses a Turbo Native Module, it is the responsibility of the app to actually generate the code using Codegen. However, when developing a TurboModule as a library, we need to reference the generated code, and it is therefore, useful to see what the app will generate.
As the first step for both iOS and Android, this guide shows how to execute manually the scripts used by Codegen to generate the required code. Further information on Codegen can be found here.
Warning
The code generated by Codegen in this step should not be committed to the versioning system. React Native apps are able to generate the code when the app is built. This allows an app to ensure that all libraries have code generated for the correct version of React Native. It is also possible to include the codegen output into your library.
To run Codegen for the iOS platform, we need to open a terminal and run the following command:
cd MyApp
yarn add ../RTNCalculator
cd ..
node MyApp/node_modules/react-native/scripts/generate-codegen-artifacts.js \
--targetPlatform ios \
--path MyApp/ \
--outputPath RTNCalculator/generated/
This script first adds the RTNCalculator
module to the app with yarn add
. Then, it invokes Codegen via the generate-codegen-artifacts.js
script.
The --path
option specifies the path to the app, while the --outputPath
option tells Codegen where to output the generated code.
The output of this process is the following folder structure:
generated
└── build
└── generated
└── ios
├── FBReactNativeSpec
│ ├── FBReactNativeSpec-generated.mm
│ └── FBReactNativeSpec.h
├── RCTThirdPartyFabricComponentsProvider.h
├── RCTThirdPartyFabricComponentsProvider.mm
├── RTNCalculatorSpec
│ ├── RTNCalculatorSpec-generated.mm
│ └── RTNCalculatorSpec.h
└── react
└── renderer
└── components
└── rncore
├── ComponentDescriptors.h
├── EventEmitters.cpp
├── EventEmitters.h
├── Props.cpp
├── Props.h
├── RCTComponentViewHelpers.h
├── ShadowNodes.cpp
└── ShadowNodes.h
The relevant path for the Turbo Native Module interface is generated/build/generated/ios/RTNCalculatorSpec
.
See the Codegen section for further details on the generated files.
Note
When generating the scaffolding code using Codegen, iOS does not clean the build
folder automatically. If you changed the Spec name, for example, and then run Codegen again, the old files would be retained.
If that happens, remember to remove the build
folder before running the Codegen again.
cd MyApp/ios
rm -rf build
Now add the Native code for your Turbo Native Module. Create two files in the RTNCalculator/ios
folder:
- The
RTNCalculator.h
, a header file for the module. - The
RTNCalculator.mm
, the implementation of the module.
#import <RTNCalculatorSpec/RTNCalculatorSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RTNCalculator : NSObject <NativeRTNCalculatorSpec>
@end
NS_ASSUME_NONNULL_END
This file defines the interface for the RTNCalculator
module. Here, we can add any native method we may want to invoke on the view. For this guide, we don't need anything, therefore the interface is empty.
#import "RTNCalculator.h"
@implementation RTNCalculator
RCT_EXPORT_MODULE()
- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithInteger:a+b];
resolve(result);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeRTNCalculatorSpecJSI>(params);
}
@end
The most important call is to the RCT_EXPORT_MODULE
, which is required to export the module so that React Native can load the Turbo Native Module.
Then the add
method, whose signature must match the one specified by the Codegen in the RTNCalculatorSpec.h
.
Finally, the getTurboModule
method gets an instance of the Turbo Native Module so that the JavaScript side can invoke its methods. The function is defined in (and requested by) the RTNCalculatorSpec.h
file that was generated earlier by Codegen.
Tip
There are other macros that can be used to export modules and methods. You view the code that specifies them here.
Android follows similar steps to iOS. We have to generate the code for Android, and then we have to write some native code to make it work.
To generate the code for Android, we need to manually invoke Codegen. This is done similarly to what we did for iOS: first, we need to add the package to the app, and then we need to invoke a script.
cd MyApp
yarn add ../RTNCalculator
cd android
./gradlew generateCodegenArtifactsFromSchema
This script first adds the package to the app, in the same way iOS does. Then, after moving to the android
folder, it invokes a Gradle task to create the generated code.
Tip
To run Codegen, you need to enable the New Architecture in the Android app. This can be done by opening the gradle.properties
files and by switching the newArchEnabled
property from false
to true
.
The generated code is stored in the MyApp/node_modules/rtn-calculator/android/build/generated/source/codegen
folder and it has this structure:
codegen
├── java
│ └── com
│ └── rtncalculator
│ └── NativeRTNCalculatorSpec.java
├── jni
│ ├── Android.mk
│ ├── CMakeLists.txt
│ ├── RTNCalculator-generated.cpp
│ ├── RTNCalculator.h
│ └── react
│ └── renderer
│ └── components
│ └── RTNCalculator
│ ├── ComponentDescriptors.h
│ ├── EventEmitters.cpp
│ ├── EventEmitters.h
│ ├── Props.cpp
│ ├── Props.h
│ ├── ShadowNodes.cpp
│ └── ShadowNodes.h
└── schema.json
The native code for the Android side of a Turbo Native Module requires:
- to create a
CalculatorModule.java
that implements the module. - to update the
CalculatorPackage.java
created in the previous step.
The final structure within the Android library should look like this:
android
├── build.gradle
└── src
└── main
└── java
└── com
└── rtncalculator
├── CalculatorModule.java
└── CalculatorPackage.java
Java
package com.rtncalculator;
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.rtncalculator.NativeRTNCalculatorSpec;
public class CalculatorModule extends NativeRTNCalculatorSpec {
public static String NAME = "RTNCalculator";
CalculatorModule(ReactApplicationContext context) {
super(context);
}
@Override
@NonNull
public String getName() {
return NAME;
}
@Override
public void add(double a, double b, Promise promise) {
promise.resolve(a + b);
}
}
Kotlin
package com.rtncalculator
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.rtncalculator.NativeRTNCalculatorSpec
class CalculatorModule(reactContext: ReactApplicationContext) : NativeRTNCalculatorSpec(reactContext) {
override fun getName() = NAME
override fun add(a: Double, b: Double, promise: Promise) {
promise.resolve(a + b)
}
companion object {
const val NAME = "RTNCalculator"
}
}
This class implements the module itself, which extends the NativeRTNCalculatorSpec
that was generated from the NativeCalculator
JavaScript specification file.
Java
package com.rtncalculator;
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.BaseReactPackage;
import java.util.Collections;
import java.util.List;
+ import java.util.HashMap;
+ import java.util.Map;
public class CalculatorPackage extends BaseReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
+ if (name.equals(CalculatorModule.NAME)) {
+ return new CalculatorModule(reactContext);
+ } else {
return null;
+ }
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
- return null;
+ return () -> {
+ final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
+ moduleInfos.put(
+ CalculatorModule.NAME,
+ new ReactModuleInfo(
+ CalculatorModule.NAME,
+ CalculatorModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ));
+ return moduleInfos;
+ };
}
}
Kotlin
package com.rtncalculator;
import com.facebook.react.BaseReactPackage
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
class CalculatorPackage : BaseReactPackage() {
- override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? = null
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
+ if (name == CalculatorModule.NAME) {
+ CalculatorModule(reactContext)
+ } else {
+ null
+ }
- override fun getReactModuleInfoProvider() = ReactModuleInfoProvider? = null
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
+ mapOf(
+ CalculatorModule.NAME to ReactModuleInfo(
+ CalculatorModule.NAME,
+ CalculatorModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ )
+ )
+ }
}
This is the last piece of Native Code for Android. It defines the TurboReactPackage
object that will be used by the app to load the module.
The final structure should look like this:
TurboModulesGuide
├── MyApp
└── RTNCalculator
├── android
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── com
│ └── rtncalculator
│ ├── CalculatorPackage.java
│ └── CalculatorModule.java
├── generated
├── ios
│ ├── RTNCalculator.h
│ └── RTNCalculator.mm
├── js
│ └── NativeCalculator.ts
├── package.json
└── rtn-calculator.podspec
Now you can install and use the Turbo Native Module in your app.
First of all, we need to add the NPM package which contains the Component to the app. This can be done with the following command:
cd MyApp
yarn add ../RTNCalculator
This command will add the RTNCalculator
module to the node_modules
of your app.
Then, you need to install the new dependencies in your iOS project. To do so, run these commands:
cd ios
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
This command will look for all the dependencies of the project and it will install the iOS ones. The RCT_NEW_ARCH_ENABLED=1
instruct CocoaPods that it has to run some additional operations to run Codegen.
Note
You may have to run bundle install
once before you can use RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
. You won't need to run bundle install
anymore, unless you need to change the Ruby dependencies.
Android configuration requires to enable the New Architecture:
- Open the
android/gradle.properties
file - Scroll down to the end of the file and switch the
newArchEnabled
property fromfalse
totrue
.
Now you can use your Turbo Native Module calculator in your app!
Here's an example App.js file using the add
method:
Flow
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import React from "react";
import { useState } from "react";
import type { Node } from "react";
import { SafeAreaView, StatusBar, Text, Button } from "react-native";
import RTNCalculator from "rtn-calculator/js/NativeCalculator";
const App: () => Node = () => {
const [result, setResult] = useState<number | null>(null);
return (
<SafeAreaView>
<StatusBar barStyle={"dark-content"} />
<Text style={{ marginLeft: 20, marginTop: 20 }}>
3+7={result ?? "??"}
</Text>
<Button
title="Compute"
onPress={async () => {
const value = await RTNCalculator.add(3, 7);
setResult(value);
}}
/>
</SafeAreaView>
);
};
export default App;
TypeScript
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from "react";
import { useState } from "react";
import { SafeAreaView, StatusBar, Text, Button } from "react-native";
import RTNCalculator from "rtn-calculator/js/NativeRTNCalculator";
const App: () => JSX.Element = () => {
const [result, setResult] = useState<number | null>(null);
return (
<SafeAreaView>
<StatusBar barStyle={"dark-content"} />
<Text style={{ marginLeft: 20, marginTop: 20 }}>
3+7={result ?? "??"}
</Text>
<Button
title="Compute"
onPress={async () => {
const value = await RTNCalculator?.add(3, 7);
setResult(value ?? null);
}}
/>
</SafeAreaView>
);
};
export default App;
Try this out to see your Turbo Native Module in action!
Important
This documentation is still experimental and details are subject to changes as we iterate. Feel free to share your feedback on this discussion.
Moreover, it contains several manual steps. Please note that this won't be representative of the final developer experience once the New Architecture is stable. We're working on tools, templates and libraries to help you get started fast on the New Architecture, without having to go through the whole setup.
Starting from React Native 0.76, Turbo Native Modules have a new, type-safe API to emit events from the Native layer to the JS layer.
This capability can be helpful to notify the JS layer about changes in the platforms (for example a change in appearence) or for tasks that ended asynchronously. It is also a great way to send events from your modules to your components.
To add this feature, we have to apply some changes at the various layers.
First of all, our specs needs to define an EventEmitter function.
Modify the specs as it follows:
Flow
// @flow
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';
+ import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
add(a: number, b: number): Promise<number>;
+ onValueChanged: EventEmitter<number>
}
export default (TurboModuleRegistry.get<Spec>(
'RTNCalculator'
): ?Spec);
TypeScript
import { TurboModule, TurboModuleRegistry } from "react-native";
+ import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
add(a: number, b: number): Promise<number>;
+ readonly onValueChanged: EventEmitter<number>
}
export default TurboModuleRegistry.get<Spec>("RTNCalculator") as Spec | null;
This new property will tell codegen to generate all the new properties and methods to wire the Event Emitter in native code
This step is the same step of the previous guide.
Just run again:
cd MyApp
yarn add ../RTNCalculator
cd ..
node MyApp/node_modules/react-native/scripts/generate-codegen-artifacts.js \
--targetPlatform ios \
--path MyApp/ \
--outputPath RTNCalculator/generated/
To see the generated output.
In this case, the Codegen will create a new base class called NativeRTNCalculatorSpecBase
that contains the glue code that allows the Event Emitter to work properly.
Just run again:
cd MyApp
yarn add ../RTNCalculator
cd android
./gradlew generateCodegenArtifactsFromSchema
For Android, the output is not much different. The classes that were generated before will now contains the new code to make the Event Emitter work.
Now that Codegen generated some new code, we need to update our existing native code to use the newly generated code.
For iOS, we need two steps:
- inherit from the newly created class
- emit the event
- Open the
RTNCalculator.h
file - Modify it as it follows:
#import <RTNCalculatorSpec/RTNCalculatorSpec.h>
NS_ASSUME_NONNULL_BEGIN
- @interface RTNCalculator : NSObject <NativeRTNCalculatorSpec>
+ @interface RTNCalculator : NativeRTNCalculatorSpecBase <NativeRTNCalculatorSpec>
@end
NS_ASSUME_NONNULL_END
- Open the
RTNCalculator.mm
file - Modify it as it follows:
#import "RTNCalculator.h"
@implementation RTNCalculator
RCT_EXPORT_MODULE()
- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithInteger:a+b];
resolve(result);
+ [self emitOnValueChanged:@(result)];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeRTNCalculatorSpecJSI>(params);
}
@end
On Android, the CalculatorModule already inherits from the right class. So, the only step required is to emit the event.
- Open the
CalculatorModule.java
file - Modify it as it follows:
public class CalculatorModule extends NativeCalculatorSpec {
public static String NAME = "RTNCalculator";
CalculatorModule(ReactApplicationContext context) {
super(context);
}
@Override
@NonNull
public String getName() {
return NAME;
}
@Override
public void add(double a, double b, Promise promise) {
promise.resolve(a + b);
+ emitOnValueChanged(a + b);
}
}
Now that all the Native Code is properly wired, we only need to listen for the event on JS side.
To do so, we can modify our app code as it follows:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from "react";
import { useState } from "react";
- import { SafeAreaView, StatusBar, Text, Button } from "react-native";
+ import { Alert, EventSubscription, SafeAreaView, StatusBar, Text, Button } from "react-native";
import RTNCalculator from "rtn-calculator/js/NativeRTNCalculator";
const App: () => JSX.Element = () => {
const [result, setResult] = useState<number | null>(null);
+ const listenerSubscription = React.useRef<null | EventSubscription>(null);
+ React.useEffect(() => {
+ listenerSubscription.current = RTNCalculator.onValueChanged((data) => { Alert.alert(`Result: ${data}`) });
+ return () => {
+ listenerSubscription.current?.remove();
+ listenerSubscription.current = null;
+ }
+ }, [])
return (
<SafeAreaView>
<StatusBar barStyle={"dark-content"} />
<Text style={{ marginLeft: 20, marginTop: 20 }}>
3+7={result ?? "??"}
</Text>
<Button
title="Compute"
onPress={async () => {
const value = await RTNCalculator?.add(3, 7);
setResult(value ?? null);
}}
/>
</SafeAreaView>
);
};
export default App;
Let's look at what we did:
- We imported the
EventSubscription
type from React Native to handle the subscription to the Event properly. - Then we defined a
listenerSubscription
object usinguseRef
to keep track of the subscription. - We used a
useEffect
hook to register to the event emitter as soon as the component is created. Notice that we are returning a cleanup closure to remove the listener and to cleanup the ref, so we don't have memory leaks in our app. - We added the listener to the module by using the
RTNCalculator.onValueChanged
method we defined in the specs. This method returns anEventSubscription
that keeps track of the listener.
Now, every time the user computes something, an Alert will pop up with the result.