Turbo Modules as Legacy Native Modules
This documentation is still experimental and details are subject to changes as we iterate. Feel free to share your feedback on the discussion inside the working group for this page.
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.
Creating a backward compatible Turbo Native Module requires the knowledge of how to create a Legacy Native Module. To recall these concepts, have a look at this guide.
TurboModules only works when the New Architecture is properly setup. If you already have a library that you want to migrate to the New Architecture, have a look at the migration guide as well.
Creating a backward compatible TurboModule lets your users continue leverage your library, independently from the architecture they use. The creation of such a module requires a few steps:
- Configure the library so that dependencies are prepared set up properly for both the Old and the New Architecture.
- Update the codebase so that the New Architecture types are not compiled when not available.
- Uniform the JavaScript API so that your user code won't need changes.
For the sake of this guide we're going to use the following terminology:
- Legacy Native Modules - To refer to Modules which are running on the old React Native architecture.
- Turbo Native Modules - To refer to Modules which have been adapted to work well with the New Native Module System. For brevity you might find them referred as Turbo Modules. :::
The TypeScript support for the New Architecture is still in beta.
While the last step is the same for all the platforms, the first two steps are different for iOS and Android.
Configure the Turbo Native Module Dependenciesβ
iOSβ
The Apple platform installs Turbo Native Modules using Cocoapods as a dependency manager.
Every Turbo Native Module defines a podspec
that looks like this:
iOSβ
The Apple platform installs TurboModules using Cocoapods as dependency manager.
Every TurboModule defines a podspec
that looks like this:
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
# Default fields for a valid podspec
s.name = "<TM 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}"
# React Native Core dependency
s.dependency "React-Core"
#Β The following lines are required by the New Architecture.
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
The goal is to avoid installing the dependencies when the app is prepared for the Old Architecture.
When we want to install the dependencies we use the following commands, depending on the architecture:
# For the Old Architecture, we use:
pod install
# For the New Architecture, we use:
RCT_NEW_ARCH_ENABLED=1 pod install
Therefore, we can leverage this environment variable in the podspec
to exclude the settings and the dependencies that are related to the New Architecture:
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
# The following lines are required by the New Architecture.
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
# ... other dependencies ...
s.dependency "ReactCommon/turbomodule/core"
+ end
end
This if
guard prevents the dependencies from being installed when the environment variable is not set.
Androidβ
To create a module that can work with both architectures, you need to configure Gradle to choose which files need to be compiled depending on the chosen architecture. This can be achieved by using different source sets in the Gradle configuration.
Please note that this is currently the suggested approach. While it might lead to some code duplication, it will ensure the maximum compatibility with both architectures. You will see how to reduce the duplication in the next section. :::
To configure the Turbo Native Module so that it picks the proper sourceset, you have to update the build.gradle
file in the following way:
+// Add this function in case you don't have it already
+ def isNewArchitectureEnabled() {
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
+}
// ... other parts of the build file
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
targetSdkVersion safeExtGet('targetSdkVersion', 31)
+ buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
+ }
+
+ sourceSets {
+ main {
+ if (isNewArchitectureEnabled()) {
+ java.srcDirs += ['src/newarch']
+ } else {
+ java.srcDirs += ['src/oldarch']
+ }
+ }
}
}
This changes do three main things:
- The first lines define a function that returns whether the New Architecture is enabled or not.
- The
buildConfigField
line defines a build configuration boolean field calledIS_NEW_ARCHITECTURE_ENABLED
, and initialize it using the function declared in the first step. This allows you to check at runtime if a user has specified thenewArchEnabled
property or not. - The last lines leverage the function declared in step one to decide which source sets we need to build, depending on the choosen architecture.
Update the codebaseβ
iOSβ
The second step is to instruct Xcode to avoid compiling all the lines using the New Architecture types and files when we are building an app with the Old Architecture.
There are two files to change. The module implementation file, which is usually a <your-module>.mm
file, and the module header, which is usually a <your-module>.h
file.
That implementation file is structured as follows:
- Some
#import
statements, among which there is a<GeneratedSpec>.h
file. - The module implementation, using the various
RCT_EXPORT_xxx
andRCT_REMAP_xxx
macros. - The
getTurboModule:
function, which uses the<MyModuleSpecJSI>
type, generated by The New Architecture.
The goal is to make sure that the Turbo Native Module
still builds with the Old Architecture. To achieve that, we can wrap the #import "<GeneratedSpec>.h"
and the getTurboModule:
function into an #ifdef RCT_NEW_ARCH_ENABLED
compilation directive, as shown in the following example:
#import "<MyModuleHeader>.h"
+ #ifdef RCT_NEW_ARCH_ENABLED
#import "<GeneratedSpec>.h"
+ #endif
// ... rest of your module
+ #ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::<MyModuleSpecJSI>>(params);
}
+ #endif
@end
A similar thing needs to be done for the header file. Add the following lines at the bottom of your module header. You need to first import the header and then, if the New Architecture is enabled, make it conform to the Spec protocol.
#import <React/RCTBridgeModule.h>
+ #ifdef RCT_NEW_ARCH_ENABLED
+ #import <YourModuleSpec/YourModuleSpec.h>
+ #endif
@interface YourModule: NSObject <RCTBridgeModule>
@end
+ #ifdef RCT_NEW_ARCH_ENABLED
+ @interface YourModule () <YourModuleSpec>
+ @end
+ #endif
This snippets uses the same RCT_NEW_ARCH_ENABLED
flag used in the previous section. When this flag is not set, Xcode skips the lines within the #ifdef
during compilation and it does not include them into the compiled binary.
Androidβ
As we can't use conditional compilation blocks on Android, we will define two different source sets. This will allow to create a backward compatible Turbo Native Module with the proper source that is loaded and compiled depending on the used architecture.
Therefore, you have to:
- Create a Legacy Native Module in the
src/oldarch
path. See this guide to learn how to create a Legacy Native Module. - Create a Turbo Native Module in the
src/newarch
path. See this guide to learn how to create a Turbo Native Module
and then instruct Gradle to decide which implementation to pick.
Some files can be shared between a Legacy Native Module and a Turbo Native Module: these should be created or moved into a folder that is loaded by both the architectures. These files are:
- the
<MyModule>Package.java
file used to load the module. - a
<MyTurboModule>Impl.java
file where we can put the code that both the Legacy Native Module and the Turbo Native Module has to execute.
The final folder structure looks like this:
my-module
βββ android
β βββ build.gradle
β βββ src
β βββ main
β β βββ AndroidManifest.xml
β β βββ java
β β βββ com
β β βββ MyModule
β β βββ MyModuleImpl.java
β β βββ MyModulePackage.java
β βββ newarch
β β βββ java
β β βββ com
β β βββ MyModule.java
β βββ oldarch
β βββ java
β βββ com
β βββ MyModule.java
βββ ios
βββ js
βββ package.json
The code that should go in the MyModuleImpl.java
, and that can be shared by the Legacy Native Module and the Turbo Native Module is, for example:
package com.MyModule;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import java.util.Map;
import java.util.HashMap;
public class MyModuleImpl {
public static final String NAME = "MyModule";
public void foo(double a, double b, Promise promise) {
// implement the logic for foo and then invoke promise.resolve or
// promise.reject.
}
}
Then, the Legacy Native Module and the Turbo Native Module can be updated with the following steps:
- Create a private instance of the
MyModuleImpl
class. - Initialize the instance in the module constructor.
- Use the private instance in the modules methods.
For example, for a Legacy Native Module:
public class MyModule extends ReactContextBaseJavaModule {
// declare an instance of the implementation
private MyModuleImpl implementation;
CalculatorModule(ReactApplicationContext context) {
super(context);
// initialize the implementation of the module
implementation = MyModuleImpl();
}
@Override
public String getName() {
// NAME is a static variable, so we can access it using the class name.
return MyModuleImpl.NAME;
}
@ReactMethod
public void foo(int a, int b, Promise promise) {
// Use the implementation instance to execute the function.
implementation.foo(a, b, promise);
}
}
And, for a Turbo Native Module:
public class MyModule extends MyModuleSpec {
// declare an instance of the implementation
private MyModuleImpl implementation;
CalculatorModule(ReactApplicationContext context) {
super(context);
// initialize the implementation of the module
implementation = MyModuleImpl();
}
@Override
@NonNull
public String getName() {
// NAME is a static variable, so we can access it using the class name.
return MyModuleImpl.NAME;
}
@Override
public void foo(double a, double b, Promise promise) {
// Use the implementation instance to execute the function.
implementation.foo(a, b, promise);
}
}
For a step-by-step example on how to achieve this, have a look at this repo.
Unify the JavaScript specsβ
The TypeScript support for the New Architecture is still in beta.
The last step makes sure that the JavaScript behaves transparently to chosen architecture.
For a Turbo Native Module, the source of truth is the Native<MyModule>.js
(or .ts
) spec file. The app accesses the spec file like this:
import MyModule from 'your-module/src/index';
Since TurboModuleRegistry.get
taps into the old Native Modules API under the hood, we need to re-export our module, to avoid registering it multiple times.
- Flow
- TypeScript
// @flow
export default require('./Native<MyModule>').default;
export default require('./Native<MyModule>').default;