Building a TurboModule
Using TurboModules to connect JavaScript to C++
In this guide we will dive into the process of building a TurboModule in more detail.
Create a TypeScript spec
Start by creating a new package
This package will contain our spec, and host the generated bridge code.
Write the spec
The spec defines the methods, events and types for the module. The spec is just a TypeScript file that you can write and save anywhere in your native package.
Here is an example of a spec for a native module called ExampleNative. This module will help us access readings from a hardware sensor that is integrated into our Unreal C++ project:
import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export type SensorMode = "slow" | "fast";
export interface SensorReading {
sensorId: string;
value: number;
}
export interface Spec extends TurboModule {
readonly onReading: CodegenTypes.EventEmitter<SensorReading>;
setEnabled(enabled: boolean): void;
getLatestValue(): number;
loadReadings(): Promise<ReadonlyArray<SensorReading>>;
}
export default TurboModuleRegistry.get<Spec>("ExampleNative");You can refer to the Codegen Typings section for a map of the types that can be used in the spec.
Provide codegen configuration in package.json
This will be used by @rnue/codegen to help it find the spec and know where to output generated boilerplate bridge code.
{
"name": "example-native",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"scripts": {
"codegen": "rnue-codegen codegen",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@rnue/codegen": "0.0.3",
"typescript": "^6.0.3"
},
"peerDependencies": {
"react": "^19.2.3",
"react-native": "^0.86.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-native": {
"optional": true
}
},
"codegenConfig": {
"name": "ExampleNative",
"type": "modules",
"jsSrcsDir": "specs",
"includesGeneratedCode": true,
"outputDir": {
"unreal": "unreal/"
}
}
}Verify
At the end of the process you should have a package with roughly the following structure:
Generate the boilerplate
Run the following command:
npm run codegenThe result will be an unreal/ folder with the following files:
Do not modify ExampleNativeSpec.h, ExampleNativeAdvanced.h, ExampleNativeBinding.cpp, or ExampleNativeJSI.h. Codegen replaces these files on each run.
Go ahead and explore the generated code. Look at ExampleNativeService.cpp. The file contains fail-fast implementations that obey these rules:
- Void and synchronous methods throw
FReactModuleError. - Typed
Promisemethods reject theirPromise. - Raw-JSI
Promisemethods throw synchronously.
Link or copy the generated bridge to the Unreal project
This step is also automatic and done by running @rnue/codegen.
Implement the native service
Your code will live in ExampleNativeService.cpp. The service extends and implements the generated spec bridge:
class FExampleNativeService final : public ExampleNative::Generated::IService
{
public:
void SetEnabled(bool bEnabled) override;
double GetLatestValue() override;
void LoadReadings(ReactNativeUnreal::TReactPromise<TArray<ExampleNative::Generated::FSensorReading>> Promise) override;
};Configure the native module
Create a file named react-native.config.js to properly configure the native module and make it discoverable by @rnue/cli:
"use strict";
module.exports = {
dependency: {
platforms: {
unreal: {
pluginPath: "./unreal/ExampleNative",
buildModules: ["ExampleNative"],
turboModules: ["ExampleNative"],
requiresPackages: {},
},
},
},
};Use the native service
To use your newly created native module from JavaScript, you have to export both the spec types and the spec default export.
import ExampleNative from "../specs/NativeExampleNative";
export type { Spec } from "../specs/NativeExampleNative";
export { ExampleNative };
export default ExampleNative;Note that ExampleNative will be null if the TurboModule is not properly initialized in Unreal Engine.
You can now import it elsewhere and call the native module:
import ExampleNative from "example-native";
if (ExampleNative === null) {
throw new Error("ExampleNative is not available.");
}
ExampleNative.setEnabled(true);
const latestValue = ExampleNative.getLatestValue();
const readings = await ExampleNative.loadReadings();