Skip to content

Commit c0bfc7a

Browse files
author
Riccardo Cipolleschi
committed
[Guide - The New Architecture] TurboModules as Native Modules
1 parent 9d37c2a commit c0bfc7a

File tree

2 files changed

+363
-5
lines changed

2 files changed

+363
-5
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:::caution
2+
The TypeScript support for the new architecture is still in beta.
3+
:::

docs/the-new-architecture/backward-compatibility-turbomodules.md

Lines changed: 360 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,365 @@ id: backward-compatibility-turbomodules
33
title: TurboModules as Native Modules
44
---
55

6-
This section describes the required steps to ensure that a TurboModule can be used as a Native Module.
6+
import Tabs from '@theme/Tabs';
7+
import TabItem from '@theme/TabItem';
8+
import constants from '@site/core/TabsConstants';
9+
import BetaTS from './\_markdown_beta_ts_support.mdx';
710

8-
The section explains:
11+
:::info
12+
The creation of a backward compatible TurboModule requires the knowledge of how to create a TurboModule. To recall these concepts, have a look at this [guide](pillars-turbomodules).
913

10-
- How to avoid installing dependencies when they are not needed
11-
- The usage of compilation pragmas to avoid compiling code that requires types from the codegen
12-
- API uniformity in JS, so that they don’t have to import different files
14+
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](../new-architecture-intro) as well.
15+
:::
16+
17+
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:
18+
19+
1. Configure the library so that dependencies are prepared set up properly for both the old and the New Architecture.
20+
1. Update the codebase so that the New Architecture types are not compiled when not available.
21+
1. Uniform the JavaScript API so that your user code won't need changes.
22+
23+
<BetaTS />
24+
25+
While the last step is the same for all the platforms, the first two steps are different for iOS and Android.
26+
27+
## Configure the TurboModule Dependencies
28+
29+
### <a name="dependencies-ios" />iOS
30+
31+
The Apple platform installs TurboModules using [Cocoapods](https://cocoapods.org) as dependency manager.
32+
33+
Every TurboModule defines a `podspec` that looks like this:
34+
35+
```ruby
36+
require "json"
37+
38+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
39+
40+
folly_version = '2021.06.28.00-v2'
41+
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
42+
43+
Pod::Spec.new do |s|
44+
# Default fields for a valid podspec
45+
s.name = "<TM Name>"
46+
s.version = package["version"]
47+
s.summary = package["description"]
48+
s.description = package["description"]
49+
s.homepage = package["homepage"]
50+
s.license = package["license"]
51+
s.platforms = { :ios => "11.0" }
52+
s.author = package["author"]
53+
s.source = { :git => package["repository"], :tag => "#{s.version}" }
54+
55+
s.source_files = "ios/**/*.{h,m,mm,swift}"
56+
# React Native Core dependency
57+
s.dependency "React-Core"
58+
59+
# The following lines are required by the New Architecture.
60+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
61+
s.pod_target_xcconfig = {
62+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
63+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
64+
}
65+
66+
s.dependency "React-Codegen"
67+
s.dependency "RCT-Folly", folly_version
68+
s.dependency "RCTRequired"
69+
s.dependency "RCTTypeSafety"
70+
s.dependency "ReactCommon/turbomodule/core"
71+
72+
end
73+
```
74+
75+
The **goal** is to avoid installing the dependencies when the app is prepared for the Old Architecture.
76+
77+
When we want to install the dependencies we use the following commands, depending on the architecture:
78+
79+
```sh
80+
# For the Old Architecture, we use:
81+
pod install
82+
83+
# For the New Architecture, we use:
84+
RCT_NEW_ARCH_ENABLED=1 pod install
85+
```
86+
87+
Therefore, we can leverage this environment variable in the `podspec` to exclude the settings and the dependencies that are related to the New Architecture:
88+
89+
```diff
90+
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
91+
# The following lines are required by the New Architecture.
92+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
93+
# ... other dependencies ...
94+
s.dependency "ReactCommon/turbomodule/core"
95+
+ end
96+
end
97+
```
98+
99+
This `if` guard prevents the dependencies from being installed when the environment variable is not set.
100+
101+
### Android
102+
103+
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.
104+
105+
:::note
106+
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.
107+
:::
108+
109+
To configure the TurboModule so that it picks the proper sourceset, you have to update the `build.gradle` file in the following way:
110+
111+
```diff title="build.gradle"
112+
+// Add this function in case you don't have it already
113+
+ def isNewArchitectureEnabled() {
114+
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
115+
+}
116+
117+
118+
// ... other parts of the build file
119+
120+
defaultConfig {
121+
minSdkVersion safeExtGet('minSdkVersion', 21)
122+
targetSdkVersion safeExtGet('targetSdkVersion', 31)
123+
+ buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
124+
+ }
125+
+
126+
+ sourceSets {
127+
+ main {
128+
+ if (isNewArchitectureEnabled()) {
129+
+ java.srcDirs += ['src/newarch']
130+
+ } else {
131+
+ java.srcDirs += ['src/oldarch']
132+
+ }
133+
+ }
134+
}
135+
}
136+
```
137+
138+
This changes do three main things:
139+
140+
1. The first lines define a function that returns whether the New Architecture is enabled or not.
141+
2. The `buildConfigField` line defines a build configuration boolean field called `IS_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 the `newArchEnabled` property or not.
142+
3. The last lines leverage the function declared in step one to decide which source sets we need to build, depending on the choosen architecture.
143+
144+
## Update the codebase
145+
146+
### iOS
147+
148+
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.
149+
150+
The file to change is the module implementation file, which is usually a `<your-module>.mm` file. That file is structured as follow:
151+
152+
- Some `#import` statements, among which there is a `<GeneratedSpec>.h` file.
153+
- The module implementation, using the various `RCT_EXPORT_xxx` and `RCT_REMAP_xxx` macros.
154+
- The `getTurboModule:` function, which uses the `<MyModuleSpecJSI>` type, generated by The New Architecture.
155+
156+
The **goal** is to make sure that the `TurboModule` 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:
157+
158+
```diff
159+
#import "<MyModuleHeader>.h"
160+
+ #ifdef RCT_NEW_ARCH_ENABLED
161+
#import "<GeneratedSpec>.h"
162+
+ #endif
163+
164+
// ... rest of your module
165+
166+
+ #ifdef RCT_NEW_ARCH_ENABLED
167+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
168+
(const facebook::react::ObjCTurboModule::InitParams &)params
169+
{
170+
return std::make_shared<facebook::react::<MyModuleSpecJSI>>(params);
171+
}
172+
+ #endif
173+
174+
@end
175+
```
176+
177+
This snippet uses the same `RCT_NEW_ARCH_ENABLED` flag used in the previous [section](#dependencies-ios). 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.
178+
179+
### Android
180+
181+
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 TurboModule with the proper source that is loaded and compiled depending on the used architecture.
182+
183+
Therefore, you have to:
184+
185+
1. Create a Native Module in the `src/oldarch` path. See [this guide](../native-modules-intro) to learn how to create a Native Module.
186+
2. Create a TurboModule in the `src/newarch` path. See [this guide](./pillars-turbomodules) to learn how to create a TurboModule
187+
188+
and then instruct Gradle to decide which implementation to pick.
189+
190+
Some files can be shared between a Native Module and a TurboModule: these should be created or moved into a folder that is loaded by both the architectures. These files are:
191+
192+
- the `<MyModule>Package.java` file used to load the module.
193+
- a `<MyTurboModule>Impl.java` file where we can put the code that both the Native Module and the TurboModule has to execute.
194+
195+
The final folder structure looks like this:
196+
197+
```sh
198+
my-module
199+
├── android
200+
│   ├── build.gradle
201+
│   └── src
202+
│   ├── main
203+
│   │ ├── AndroidManifest.xml
204+
│   │ └── java
205+
│   │ └── com
206+
│   │ └── MyModule
207+
│   │ ├── MyModuleImpl.java
208+
│   │ └── MyModulePackage.java
209+
│ ├── newarch
210+
│ │ └── java
211+
│   │ └── com
212+
│ │ └── MyModule.java
213+
│ └── oldarch
214+
│ └── java
215+
│   └── com
216+
│ └── MyModule.java
217+
├── ios
218+
├── js
219+
└── package.json
220+
```
221+
222+
The code that should go in the `MyModuleImpl.java` and that can be shared by the Native Module and the TurboModule is, for example:
223+
224+
```java title="example of MyModuleImple.java"
225+
package com.MyModule;
226+
227+
import androidx.annotation.NonNull;
228+
import com.facebook.react.bridge.Promise;
229+
import java.util.Map;
230+
import java.util.HashMap;
231+
232+
public class MyModuleImpl {
233+
234+
public static final String NAME = "MyModule";
235+
236+
public void foo(double a, double b, Promise promise) {
237+
// implement the logic for foo and then invoke promise.resolve or
238+
// promise.reject.
239+
}
240+
}
241+
```
242+
243+
Then, the Native Module and the TurboModule can be updated with the following steps:
244+
245+
1. Create a private instance of the `MyModuleImpl` class.
246+
2. Initialize the instance in the module constructor.
247+
3. Use the private instance in the modules methods.
248+
249+
For example, for a Native Module:
250+
251+
```java title="Native Module using the Impl module"
252+
public class MyModule extends ReactContextBaseJavaModule {
253+
254+
// declare an instance of the implementation
255+
private MyModuleImpl implementation;
256+
257+
CalculatorModule(ReactApplicationContext context) {
258+
super(context);
259+
// initialize the implementation of the module
260+
implementation = MyModuleImpl();
261+
}
262+
263+
@Override
264+
public String getName() {
265+
// NAME is a static variable, so we can access it using the class name.
266+
return MyModuleImpl.NAME;
267+
}
268+
269+
@ReactMethod
270+
public void foo(int a, int b, Promise promise) {
271+
// Use the implementation instance to execute the function.
272+
implementation.foo(a, b, promise);
273+
}
274+
}
275+
```
276+
277+
And, for a TurboModule:
278+
279+
```java title="TurboModule using the Impl module"
280+
public class MyModule extends MyModuleSpec {
281+
// declare an instance of the implementation
282+
private MyModuleImpl implementation;
283+
284+
CalculatorModule(ReactApplicationContext context) {
285+
super(context);
286+
// initialize the implementation of the module
287+
implementation = MyModuleImpl();
288+
}
289+
290+
@Override
291+
@NonNull
292+
public String getName() {
293+
// NAME is a static variable, so we can access it using the class name.
294+
return MyModuleImpl.NAME;
295+
}
296+
297+
@Override
298+
public void foo(double a, double b, Promise promise) {
299+
// Use the implementation instance to execute the function.
300+
implementation.foo(a, b, promise);
301+
}
302+
}
303+
```
304+
305+
For a step-by-step example on how to achieve this, have a look at [this repo](https://github.com/react-native-community/RNNewArchitectureLibraries/tree/feat/back-turbomodule).
306+
307+
## Unify the JavaScript specs
308+
309+
<BetaTS />
310+
311+
The last step makes sure that the JavaScript behaves transparently to chosen architecture.
312+
313+
For a TurboModule, the source of truth is the `Native<MyModule>.js` (or `.ts`) spec file. The app accesses the spec file like this:
314+
315+
```ts
316+
import MyModule from 'your-module/src/index';
317+
```
318+
319+
The **goal** is to conditionally `export` from the `index` file the proper object, given the architecture chosen by the user. We can achieve this with a code that looks like this:
320+
321+
<Tabs groupId="turbomodule-backward-compatibility"
322+
defaultValue={constants.defaultTurboModuleSpecLanguage}
323+
values={constants.turboModuleSpecLanguages}>
324+
<TabItem value="Flow">
325+
326+
```ts
327+
// @flow
328+
329+
import { NativeModules } from 'react-native'
330+
331+
const isTurboModuleEnabled = global.__turboModuleProxy != null;
332+
333+
const myModule = isTurboModuleEnabled ?
334+
require('./Native<MyModule>').default :
335+
NativeModules.<MyModule>;
336+
337+
export default myModule;
338+
```
339+
340+
</TabItem>
341+
<TabItem value="TypeScript">
342+
343+
```ts
344+
const isTurboModuleEnabled = global.__turboModuleProxy != null;
345+
346+
const myModule = isTurboModuleEnabled
347+
? require('./Native<MyModule>').default
348+
: require('./<MyModule>').default;
349+
350+
export default myModule;
351+
```
352+
353+
</TabItem>
354+
</Tabs>
355+
356+
:::note
357+
If you are using TypeScript and you want to follow the example, make sure to `export` the `NativeModule` in a separate `ts` file called `<MyModule>.ts`.
358+
:::
359+
360+
Whether you are using Flow or TypeScript for your specs, we understand which architecture is running by checking whether the `global.__turboModuleProxy` object has been set or not.
361+
362+
:::caution
363+
The `global.__turboModuleProxy` API may change in the future for a function that encapsulate this check.
364+
:::
365+
366+
- If that object is `null`, the app has not enabled the TurboModule feature. It's running on the Old Architecture, and the fallback is to use the default [`NativeModule` implementation](../native-modules-intro).
367+
- If that object is set, the app is running with the TurboModules enabled and it should use the `Native<MyModule>` spec to access the TurboModule.

0 commit comments

Comments
 (0)