When migrating a legacy React Native iOS module built on RCT_EXPORT_MODULE and RCT_EXPORT_METHOD to a Turbo Module, what replaces each macro?
answer
- +load registers into a global list
- the spec now lists the methods
- modulesProvider replaces the macro's registration
- write +moduleName by hand
- interop layer still loads old modules
basics
~20 sRCT_EXPORT_MODULE's two jobs split: you write +moduleName yourself and register the class under codegenConfig.ios in package.json. RCT_EXPORT_METHOD disappears: the TypeScript spec declares each method and the class implements the matching selector from the generated protocol.
solid answer
~40 s`RCT_EXPORT_MODULE(Name)` expanded to a `+moduleName` method plus a `+load` hook that called `RCTRegisterModule`, adding the class to a global registry at launch. In a Turbo Module you implement `+moduleName` explicitly and register the class through `codegenConfig.ios` (`modulesProvider` maps `"NativeSmartLock"` to `"RCTSmartLock"`), from which Codegen generates the provider map. `RCT_EXPORT_METHOD` marked a method as callable and always asynchronous; now the method list comes from the TypeScript spec, and you implement the selector the generated protocol declares — `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` becomes a spec method with a plain return type, and `RCTConvert` argument conversion moves into the method body. Until migrated, the interop layer still loads the old module in 0.87.
code
objectivec · 18 lines// Before: legacy module, RCTSmartLock.m
#import <React/RCTBridgeModule.h>
@interface RCTSmartLock : NSObject <RCTBridgeModule>
@end
@implementation RCTSmartLock
RCT_EXPORT_MODULE(SmartLock);
RCT_EXPORT_METHOD(unlock:(NSString *)lockId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
resolve(nil);
}
@endgo deeper
Know that the New Architecture replaces the export macros with a typed spec and a package.json registration, and that +moduleName is written by hand.
Explain both jobs RCT_EXPORT_MODULE did, name the replacement for each legacy macro, and walk through regenerating code with pod install.
Plan a migration of several modules: find stragglers with RCTLegacyWarningsEnabled, keep JavaScript call sites stable, and watch for sync exports that now block JavaScript.
Weigh migrating in-house modules now against leaning on the interop layer, which React Native keeps for the foreseeable future while the Legacy Architecture itself has been frozen since 0.80.
## What the legacy macros actually did Before the New Architecture, an iOS **native module** was an Objective-C class that adopted `RCTBridgeModule` and used macros to announce itself: - **`RCT_EXPORT_MODULE(js_name)`** expands to two methods. `+moduleName` returns the JavaScript-facing name (with no argument, the class name minus its `RCT` prefix is used), and `+load` calls `RCTRegisterModule(self)`, which adds the class to a **global list of module classes** the moment the Objective-C runtime loads it. - **`RCT_EXPORT_METHOD(selector)`** generates metadata that makes one method callable from JavaScript. Such methods are **asynchronous** and return `void`; results travel back through callbacks, promises or events. - **`RCT_REMAP_METHOD(js_name, selector)`** does the same with a different JavaScript name, and **`RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD`** exports a method that returns a value synchronously. - **`RCT_EXTERN_MODULE` / `RCT_EXTERN_METHOD`** were the Swift route: a `.m` file declared the Swift class's methods so the bridge could see them. Arguments were often converted with `RCTConvert` helpers keyed off the declared parameter types. ## The one-to-one replacement table | Legacy piece | Turbo Module replacement | |---|---| | `RCT_EXPORT_MODULE(SmartLock)` — name | An explicit `+ (NSString *)moduleName` returning `@"NativeSmartLock"` | | `RCT_EXPORT_MODULE` — `+load` registration | An entry under `codegenConfig.ios` in `package.json`; Codegen writes `RCTModuleProviders.mm` from it | | `RCT_EXPORT_METHOD(...)` | A method in the TypeScript spec plus the matching selector from the generated `NativeSmartLockSpec` protocol | | `RCT_REMAP_METHOD(jsName, ...)` | Name the spec method what JavaScript should call | | `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` | A spec method with a non-`void`, non-`Promise` return type | | `RCTConvert` argument conversion | Convert inside the method body; React Native's docs discourage `RCTConvert` | | `RCT_EXTERN_MODULE` / `RCT_EXTERN_METHOD` for Swift | A thin Objective-C++ class forwarding to a Swift adapter | | `NativeModules.SmartLock` in JavaScript | The spec file's default export from `TurboModuleRegistry.getEnforcing` | The shift in one line: the **source of truth for the method list moves from macros in native code to a typed spec in JavaScript**, and registration moves from a runtime side effect to generated code. ## A migration, step by step 1. **Write the spec** for the existing JavaScript surface, keeping method names so callers do not change beyond the import. 2. **Run `pod install`** so Codegen generates the protocol and the `...SpecJSI` class. 3. **Rename the implementation to `.mm`**, adopt `NativeSmartLockSpec` in the header, and add `getTurboModule:` returning `std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params)`. 4. **Delete the macros.** Replace `RCT_EXPORT_MODULE` with a hand-written `+moduleName`; turn each `RCT_EXPORT_METHOD` body into a plain method whose signature matches the protocol. 5. **Register the class** under `codegenConfig.ios` (the tutorial's `modulesProvider` map, or the per-module `ios.modules.<Name>.className` form in the Codegen reference) and run `pod install` again. 6. **Update the JavaScript call sites** to import the spec instead of reading `NativeModules`. The official Swift guide shows the removal explicitly: `RCT_EXPORT_MODULE` is deleted from the `.mm` and `+moduleName` is added, because registration now happens through `package.json`. ## Why the old module still runs meanwhile React Native 0.82 made the New Architecture the only architecture, but legacy modules are not dead on arrival: an **interop layer**, which the default app setup turns on, wraps `RCT_EXPORT_MODULE` classes so they keep working. The 0.82 release post says the interop layers stay for the foreseeable future, and 0.84 compiled the Legacy Architecture itself out of iOS builds while keeping them. To find stragglers, set `RCTLegacyWarningsEnabled` in `Info.plist`; debug builds then log a warning listing every module still registered with `RCT_EXPORT_MODULE`. A migrated module gains typed arguments, lazy lookup by name, and a generated protocol that makes drift between the TypeScript and Objective-C sides visible at build time instead of at the first call. ## Traps during migration - **Keeping `RCT_EXPORT_MODULE` "just in case"** — it keeps the `+load` registration alive alongside the new provider map, which is exactly the legacy path the warning is designed to flag. - **Porting `RCTConvert`-typed parameters verbatim** — the generated protocol uses Foundation types, `double` for numbers and `JS::` structs for typed objects, so signatures change. - **Turning an asynchronous export into a value-returning spec method for convenience** — value-returning spec methods are called synchronously, so a slow body now blocks JavaScript while it waits. - **Renaming the module during the migration** — a new name must change in `+moduleName`, the `codegenConfig.ios` key and the `getEnforcing` call together, or JavaScript cannot find the module. - **Migrating a third-party library by hand** — upgrade it or ask its authors instead; a patched fork of someone else's native code is a long-term maintenance cost.
- How do you list the modules in an app that still use the legacy registration?Add the `RCTLegacyWarningsEnabled` key set to true in the app's `Info.plist` and run a debug build. React Native then logs one warning listing every module registered with `RCT_EXPORT_MODULE`, pointing at the registration docs, so you can migrate them or push the library authors.
- What happens to a method that was exported with RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD?It becomes a spec method with an ordinary return type, such as `sdkVersion(): string`, and the generated selector returns `NSString *`. The call is still synchronous, so its body runs while JavaScript waits; keep it to cheap, in-memory work.
saying these in an interview costs you the question
- A Turbo Module still needs RCT_EXPORT_METHOD on every method to be visible
- RCT_EXPORT_MODULE is required so the class is registered with the runtime
- Legacy modules stop loading entirely the moment an app runs on 0.82 or later
- RCT_EXPORT_METHOD methods can return values directly like any Objective-C method
- Keeping RCTConvert parameter types is the recommended way to port arguments