This documentation is based on React Native documentation licensed under the CC-BY-4.0 license at https://reactnative.cn/docs/0.72/the-new-architecture/pillars-turbomodules. © Meta Platforms Inc. Changed to How to create a TurboModule on OpenHarmony.
Implementation of a Custom TurboModule
Implementation of ArkTS TurboModule
Directory Structure
You can declare TurboModule as a module and add it to your project as a dependency.
You can create a RTNCalculator directory at the same level as the MyApp directory of the React Native project, create a src/specs subdirectory, and create the v1 and v2 subdirectories based on the Codegen version. The directory structure is as follows:
├── MyApp
└── RTNCalculator
└── src
└── specs
├── v1
└── v2
JavaScript API Declaration
Create index.ts in the RTNCalculator directory and export the API declaration file. The NativeCalculator.ts file is stored in the v1 or v2 directory as required. In this example, the file is stored in the V2 directory. The API declaration file can be compiled using the Flow or TypeScript language.
// index.ts
import NativeCalculator from "./src/specs/v2/NativeCalculator";
export const RTNCalculator = NativeCalculator;
npmdoes not pack empty folders. You need to create a.gitkeepfile to save the v1 directory.
Note that the file must meet the following requirements. For details, see React Native.
- The file must be named using
Native<MODULE_NAME>. Codegen finds only the files that match these naming rules. - The
TurboModuleRegistrySpecobject must be output from the code.
// NativeCalculator.ts
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',
) as Spec | null;
Module Configuration
Add package.json to the RTNCalculator directory.
{
"name": "rtn-calculator",
"version": "1.0.0",
"description": "Add numbers with TurboModules",
"main": "index.ts",
"keywords": [],
"author": "<Your Name> <your_email@your_provider.com> (https://github.com/<your_github_handle>)",
"license": "ISC",
"harmony": {
"alias": "rtn-calculator",
"codegenConfig": [
{
"version": 1,
"specPaths": [
"./src/specs/v1"
]
},
{
"version": 2,
"specPaths": [
"./src/specs/v2"
]
}
]
},
"files": [
"index.ts",
"src/*",
"harmony.tar.gz"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"devDependencies": {
"@types/react": "^19.2.0",
"react": "19.2.3",
"react-native": "0.84.1"
},
"dependencies": {}
}
The preceding file contains some common descriptive information, such as the package name, version, and author information. You need to set the placeholder by using <>.
In addition, the configuration declaration of OpenHarmony uses the harmony field, which contains the following two fields:
alias: alias of the module.codegenConfig: file for storing the object array of a third-party library to be generated. Each object contains two fields:version: Codegen version used by the third-party library. Values: {1, 2}.specPaths: relative path of the API declaration file.
Native Code
Using Codegen to Generate Native Code
For details about how to use Codegen, see Codegen.
-
Add commands to the original project.
Add the Codegen-related scripts to
package.jsoninMyApp.{ "name": "MyApp", "version": "0.0.1", "private": true, "scripts": { ··· + "codegen": "react-native codegen-harmony --cpp-output-path ./entry/src/main/cpp/generated --rnoh-module-path ./entry/oh_modules/@rnoh/react-native-openharmony" }, ··· } -
Call the scripts to generate code.
npmdoes not pack empty folders. You can retain the directory by implementing.gitkeep.cd RTNCalculator npm pack cd ../MyApp npm i file:../RTNCalculator/rtn-calculator-1.0.0.tgz npm run codegen
After the execution is successful, copy the generated glue code to your OpenHarmony project.
Writing Native OpenHarmony Code
-
Create a
CalculatorModule.tsfile.Create the
turbomodulefolder in theentry/src/main/etsdirectory and addCalculatorModule.tsto the folder. For CxxTurboModule, you do not need to implement it here. Leave the function body empty. For ArkTSTurboModule, add the following code:// entry/src/main/ets/turbomodule/CalculatorModule.ts import { TurboModule } from '@rnoh/react-native-openharmony/ts'; import { TM } from '@rnoh/react-native-openharmony/generated/ts'; export class CalculatorModule extends TurboModule implements TM.RTNCalculator.Spec { add(a: number, b: number): Promise<number>{ return Promise.resolve(a+b); } }For CxxTurboModule, you do not need to implement it here. You only need to ensure that the parameter type and return type of the function are correct.
// entry/src/main/ets/turbomodule/CalculatorModule.ts import { TurboModule } from '@rnoh/react-native-openharmony/ts'; import { TM } from '@rnoh/react-native-openharmony/generated/ts'; export class CalculatorModule extends TurboModule implements TM.RTNCalculator.Spec { add(a: number, b: number): Promise<number>{ return Promise.resolve(123456); } } -
Create the implementation file
GeneratedPackage.etsof the package.Create
GeneratedPackage.etsin theentry/src/main/etsdirectory and add the following code:// entry/src/main/ets/GeneratedPackage.ets import { RNPackage, TurboModulesFactory } from '@rnoh/react-native-openharmony/ts'; import type { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts'; import { TM } from "@rnoh/react-native-openharmony/generated/ts" import { CalculatorModule } from './turbomodule/CalculatorModule'; class GeneratedTurboModulesFactory extends TurboModulesFactory { createTurboModule(name: string): TurboModule | null { if (name === TM.RTNCalculator.NAME) { return new CalculatorModule(this.ctx); } return null; } hasTurboModule(name: string): boolean { return name === TM.RTNCalculator.NAME; } } export class GeneratedPackage extends RNPackage { createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory { return new GeneratedTurboModulesFactory(ctx); } }Note the following:
- Use
exportfor the class that inheritsRNPackage. The class implementscreateTurboModulesFactory, which is used to create the factory class of TurboModule. - The following two methods must be implemented in the project class:
createTurboModule: creates the TurboModulele class based on the name.hasTurboModule: determines whether the TurboModule corresponding to the name exists.
- Add the method of creating the package to
entry/src/main/ets/RNPackagesFactory.ets.
import { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony/ts'; import {SamplePackage} from '@rnoh/sample-package/src/main/ets/SamplePackage'; + import { GeneratedPackage } from './GeneratedPackage'; export function createRNPackages(ctx: RNPackageContext): RNPackage[] { return [ new SamplePackage(ctx), + new GeneratedPackage(ctx) ]; } - Use
-
For CxxTurboModule, some additional steps are required:
For details about CxxTurbomodule, expand the macro
ARK_ASYNC_METHOD_METADATAin the glue code fileRTNCalculator.cpp.#include "RTNCalculator.h" namespace rnoh { using namespace facebook; RTNCalculator::RTNCalculator(const ArkTSTurboModule::Context ctx, const std::string name) : ArkTSTurboModule(ctx, name) { methodMap_ = { - ARK_ASYNC_METHOD_METADATA(add, 2), + { "add", + { 2, + [] (facebook::jsi::Runtime& rt, facebook::react::TurboModule& turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast<ArkTSTurboModule&>(turboModule).callAsync(rt, "add", args, count); + } + } + } }; } } // namespace rnohDelete the asynchronous bridge call to ArkTSTurboModule from the lambda expression and perform direct implementation.
#include "RTNCalculator.h" namespace rnoh { using namespace facebook; RTNCalculator::RTNCalculator(const ArkTSTurboModule::Context ctx, const std::string name) : ArkTSTurboModule(ctx, name) { methodMap_ = { { "add", { 2, [] (facebook::jsi::Runtime& rt, facebook::react::TurboModule& turboModule, const facebook::jsi::Value* args, size_t count) { - return static_cast<ArkTSTurboModule&>(turboModule).callAsync(rt, "add", args, count); + return jsi::Value(args[0].asNumber() + args[1].asNumber()); } } } }; } } // namespace rnoh -
The subsequent steps vary according to the Codegen version in use:
V1 Version
-
Add the new glue code file to
CMakeLists.txt.Define the generated path of .cpp file in
entry/src/main/cpp/CMakeLists.txtand add the path to the build.project(rnapp) cmake_minimum_required(VERSION 3.4.1) set(CMAKE_SKIP_BUILD_RPATH TRUE) set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp") set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated") set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments") set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie") add_compile_definitions(WITH_HITRACE_SYSTRACE) set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use add_subdirectory("${RNOH_CPP_DIR}" ./rn) # RNOH_BEGIN: add_package_subdirectories add_subdirectory("${OH_MODULE_DIR}/@rnoh/sample-package/src/main/cpp" ./sample-package) # RNOH_END: add_package_subdirectories + file(GLOB GENERATED_CPP_FILES "./generated/*.cpp") add_library(rnoh_app SHARED + ${GENERATED_CPP_FILES} "./PackageProvider.cpp" "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" ) target_link_libraries(rnoh_app PUBLIC rnoh) # RNOH_BEGIN: link_packages target_link_libraries(rnoh_app PUBLIC rnoh_sample_package) # RNOH_END: link_packages -
Create the package object of CPP in
entry/src/main/cpp/PackageProvider.cpp.#include "RNOH/PackageProvider.h" #include "SamplePackage.h" + #include "generated/RNOHGeneratedPackage.h" using namespace rnoh; std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) { return { + std::make_shared<RNOHGeneratedPackage>(ctx), std::make_shared<SamplePackage>(ctx) }; }
V2 Version
-
Add the new glue code file to
CMakeLists.txt.In this step, use
add_libraryandtarget_include_directoriesto add the generated Codegen files to the build task in the CMake file.project(rnapp) cmake_minimum_required(VERSION 3.4.1) set(CMAKE_SKIP_BUILD_RPATH TRUE) set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp") + set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated") set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments") set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie") add_compile_definitions(WITH_HITRACE_SYSTRACE) set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use add_subdirectory("${RNOH_CPP_DIR}" ./rn) # RNOH_BEGIN: add_package_subdirectories add_subdirectory("${OH_MODULE_DIR}/@rnoh/sample-package/src/main/cpp" ./sample-package) # RNOH_END: add_package_subdirectories + set(rtn_calculator_generated_dir "${RNOH_GENERATED_DIR}/rtn_calculator") + file(GLOB_RECURSE rtn_calculator_generated_dir_SRC "${rtn_calculator_generated_dir}/**/*.cpp") + file(GLOB rtn_calculator_package_SRC CONFIGURE_DEPENDS *.cpp) add_library(rnoh_app SHARED + ${rtn_calculator_generated_dir_SRC} + ${rtn_calculator_package_SRC} "./PackageProvider.cpp" "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" ) + target_include_directories(rnoh_app PUBLIC ${rtn_calculator_generated_dir}) target_link_libraries(rnoh_app PUBLIC rnoh) # RNOH_BEGIN: link_packages target_link_libraries(rnoh_app PUBLIC rnoh_sample_package) # RNOH_END: link_packages -
Create the package object of CPP in
entry/src/main/cpp/PackageProvider.cpp.#include "RNOH/PackageProvider.h" #include "SamplePackage.h" + #include "generated/rtn_calculator/RNOH/generated/BaseRtnCalculatorPackage.h" using namespace rnoh; std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) { return { std::make_shared<SamplePackage>(ctx), + std::make_shared<BaseRtnCalculatorPackage>(ctx) }; }
Setting the Custom TurboModule to Run in the Worker Thread
-
To create a TurboModule that runs in the worker thread, perform the following steps:
// entry/src/main/ets/GeneratedPackage.ets import { RNPackage, AnyThreadTurboModuleFactory } from '@rnoh/react-native-openharmony/ts'; import type { AnyThreadTurboModule, AnyThreadTurboModuleContext } from '@rnoh/react-native-openharmony/ts'; import { TM } from "@rnoh/react-native-openharmony/generated/ts" import { CalculatorModule } from './turbomodule/CalculatorModule'; class GeneratedTurboModulesFactory extends AnyThreadTurboModuleFactory { createTurboModule(name: string): AnyThreadTurboModule | null { if (name === TM.RTNCalculator.NAME) { return new CalculatorModule(this.ctx); } return null; } hasTurboModule(name: string): boolean { return name === TM.RTNCalculator.NAME; } } export class GeneratedPackage extends RNPackage { createAnyThreadTurboModuleFactory(ctx: AnyThreadTurboModuleContext): AnyThreadTurboModuleFactory { return new GeneratedTurboModulesFactory(ctx); } }To run the
CalculatorModule.etsmethod on the worker thread, add the corresponding implementation code:// entry/src/main/ets/turbomodule/CalculatorModule.ets import { AnyThreadTurboModule } from '@rnoh/react-native-openharmony/ts'; import { TM } from '@rnoh/react-native-openharmony/generated/ts'; export class CalculatorModule extends AnyThreadTurboModule implements TM.RTNCalculator.Spec { add(a: number, b: number): Promise<number>{ return Promise.resolve(a+b); } ... }Note that:
GeneratedPackageshould inherit theRNPackageclass, andcreateAnyThreadTurboModuleFactoryshould be implemented, to createAnyThreadTurboModuleFactory.- The type of the ctx parameter of the
createAnyThreadTurboModuleFactorymethod should be set toAnyThreadTurboModuleContext, and the return value type should beAnyThreadTurboModuleFactory.
- The type of the ctx parameter of the
GeneratedTurboModulesFactoryshould inheritAnyThreadTurboModuleFactory.- The following two methods must be implemented in
GeneratedTurboModulesFactory:createTurboModule: used to create the correspondingAnyThreadTurboModuleclass based on the name or returnsnull.hasTurboModule: used to determine whether the TurboModule corresponding to the name exists.
- The
CalculatorModuleclass of TurboModule should inheritAnyThreadTurboModule.
-
Configure TurboModule to run in the worker thread.
getRNOHWorkerScriptUrlshould be reloaded after RNability is inherited. The code is modified as follows:// entry/src/main/ets/entryability/EntryAbility.ets import {RNAbility} from '@rnoh/react-native-openharmony'; export default class EntryAbility extends RNAbility { + override getRNOHWorkerScriptUrl() { + return "entry/ets/workers/RNOHWorker.ets" + } ... }Right-click the ets directory, choose
New, and selectWorkerin the menu on the right.
Enter RNOHWorker.etsin the window that is displayed.
The directory structure is as follows:└── ets ├── entryability ├── page ├── rn └── workers └── RNOHWorker.ets
when you need to enable Worker threads to optimize the performance of React Native applications, you must configure the core parameters of Worker threads through RNOHWorker.ets. Below is the complete configuration code, parameter descriptions (including optional HttpClient customization and caPathProvider certificate path rule configuration), and a clear explanation of the independence between thread configurations—plus how to use rnInstanceName to align logic with the main thread.
1. Complete Configuration Code
// entry/src/main/ets/worker/RNOHWorker.ets
import { setupRNOHWorker } from "@rnoh/react-native-openharmony/src/main/ets/setupRNOHWorker";
import { createRNPackages } from '../RNPackagesFactory';
// (Optional) Import HttpClient and CAPathProvider types/implementations as needed
import type { HttpClient, CAPathProvider } from "@rnoh/react-native-openharmony/src/main/ets/types";
// --------------------------
// (Optional) 1. Custom HttpClient Implementation (for extended network capabilities)
// --------------------------
const createCustomHttpClient = (rnInstanceName: string): HttpClient => {
// Return different HttpClient configurations based on the instance name
if (rnInstanceName === "mainInstance") {
return {
addResponseInterceptor(interceptor) {
console.log(`[${rnInstanceName}] Worker Thread - Register main instance response interceptor`);
},
addRequestInterceptor(interceptor) {
console.log(`[${rnInstanceName}] Worker Thread - Register main instance request interceptor`);
},
sendRequest(url: string, requestOptions) {
console.log(`[${rnInstanceName}] Worker Thread - Send main instance request: ${url}`);
return {
cancel: () => console.log(`[${rnInstanceName}] Cancel request`),
promise: Promise.resolve({ statusCode: 200, data: "Main instance response", headers: {} })
};
},
clearCookies: () => Promise.resolve(true)
};
} else if (rnInstanceName === "secondaryInstance") {
return {
addResponseInterceptor(interceptor) {
console.log(`[${rnInstanceName}] Worker Thread - Register secondary instance response interceptor`);
},
addRequestInterceptor(interceptor) {
console.log(`[${rnInstanceName}] Worker Thread - Register secondary instance request interceptor`);
},
sendRequest(url: string, requestOptions) {
console.log(`[${rnInstanceName}] Worker Thread - Send secondary instance request: ${url}`);
return {
cancel: () => console.log(`[${rnInstanceName}] Cancel request`),
promise: Promise.resolve({ statusCode: 200, data: "Secondary instance response", headers: {} })
};
},
clearCookies: () => Promise.resolve(true)
};
}
// Default configuration
return {
addResponseInterceptor: () => {},
addRequestInterceptor: () => {},
sendRequest: () => ({ cancel: () => {}, promise: Promise.resolve({ statusCode: 200, data: "", headers: {} }) }),
clearCookies: () => Promise.resolve(true)
};
};
// --------------------------
// (Optional) 2. Custom caPathProvider (for instance-specific certificate rules)
// --------------------------
const createCustomCaPathProvider = (rnInstanceName: string): CAPathProvider => {
// Return certificate rules based on the instance name
return (url: string) => {
if (rnInstanceName === "secureInstance") {
// Secure instance: Force dedicated certificate
return "/data/haps/secure.cer";
} else if (rnInstanceName === "testInstance") {
// Test instance: Dynamic switch based on domain
return url.includes("test.com") ? "/data/haps/test.cer" : "";
}
// Default instance: Use system certificate
return "";
};
};
// --------------------------
// 3. Core: Initialize RNOH Worker Thread (use rnInstanceName for aligned configs)
// --------------------------
setupRNOHWorker({
createWorkerRNInstanceConfig: (rnInstanceName) => {
// rnInstanceName matches the instance name from the main thread
return {
// Required: Third-party **RN** package factory (registers custom **RN** components/modules)
thirdPartyPackagesFactory: createRNPackages,
// Optional: Generate HttpClient based on instance name
httpClient: createCustomHttpClient(rnInstanceName),
// Optional: Generate CA certificate rules based on instance name
caPathProvider: createCustomCaPathProvider(rnInstanceName)
}
}
})
2. Key Parameter Explanations
createWorkerRNInstanceConfig is the core function for configuring Worker threads. Its parameter rnInstanceName is critical for aligning configurations with the main thread. Below is a detailed breakdown:
| Parameter/Function | Type/Role | Key Notes |
|---|---|---|
createWorkerRNInstanceConfig |
Callback function that receives rnInstanceName and returns a WorkerRNInstanceConfig object |
Dynamically generates Worker thread configurations. rnInstanceName matches the corresponding RN instance name from the main thread, enabling "instance-specific customization". |
rnInstanceName |
string (name of the RN instance associated with the current Worker) |
1. Matches the instance name used in the main thread’s RNInstanceConfig (e.g., "main", "moduleA"); 2. Enables differentiation between multiple instances, ensuring Worker configurations align with the main thread’s logic for each instance. |
thirdPartyPackagesFactory |
(ctx: RNPackageContext) => RNPackage[] |
Required. A factory function for registering custom RN components/modules—Worker threads need this to load third-party RN capabilities. |
httpClient |
HttpClient (optional) |
Only takes effect in the Worker thread. Use rnInstanceName to generate network configurations that match the main thread’s corresponding instance (e.g., different interceptors for different instances). |
caPathProvider |
(url: string) => string (optional) |
Only takes effect in the Worker thread. Use rnInstanceName to generate certificate rules that match the main thread’s corresponding instance (e.g., different certificate paths for different instances). |
3. Using rnInstanceName to Align Thread Configurations
In multi-instance scenarios (e.g., apps with multiple RN instances), rnInstanceName ensures that Worker thread configurations logically align with the main thread. Here’s its core value:
Scenario: Differentiated Configurations for Multi-Instance Apps
Suppose the main thread has two RN instances:
- Instance 1:
rnInstanceName = "payment"(payment module, requiring strict certificate validation) - Instance 2:
rnInstanceName = "news"(news module, using default certificates)
You can use rnInstanceName to implement matching configurations in the Worker thread:
createWorkerRNInstanceConfig: (rnInstanceName) => {
return {
thirdPartyPackagesFactory: createRNPackages,
// Payment instance uses dedicated certificate; news instance uses default
caPathProvider: (url) => {
if (rnInstanceName === "payment") {
return "/data/haps/payment_cert.cer"; // Aligns with main thread’s payment instance config
}
return ""; // News instance uses default (matches main thread)
},
// Payment instance adds encryption interceptors; news instance does not
httpClient: rnInstanceName === "payment" ? paymentHttpClient : defaultHttpClient
}
}
Core Value of rnInstanceName
- Configuration Alignment: Ensures
httpClientandcaPathProviderin the Worker thread follow the same logic as the main thread’s corresponding instance (via matchingrnInstanceName). - Multi-Instance Isolation: Prevents cross-interference between configurations of different instances (e.g., strict certificate rules for payment vs. default rules for news).
- Code Reusability: Enables reusable configuration factories (based on
rnInstanceName) that work for both the main thread and Worker threads, reducing redundancy.
4. Independence Between Main Thread and Worker Thread Configurations
While rnInstanceName aligns logical rules between threads, the actual configurations (e.g., httpClient implementations) remain fully independent. No "fallback" or "override" occurs between threads:
| Configuration Item | Main Thread Configuration Location | Worker Thread Configuration Location | Independence Rules |
|---|---|---|---|
HttpClient |
RNInstanceConfig (when initializing main-thread RN instances) |
WorkerRNInstanceConfig (configured in this file) |
1. After enabling Workers, network requests in the Worker thread only use its own httpClient config; 2. If no httpClient is configured for the Worker, it will not use the main thread’s config (even if present)—only default network capabilities are available. |
caPathProvider |
RNInstanceConfig (main-thread RN instance config) |
WorkerRNInstanceConfig (configured in this file) |
1. After enabling Workers, HTTPS requests in the Worker thread only use its own caPathProvider config; 2. If no caPathProvider is configured for the Worker, it will not use the main thread’s config (even if present)—only system default certificates are used. |
TurboModule Usage
Now, you can use TurboModule in your application. The following is an example:
/**
* 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';
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;