In React Native 0.87, what must an iOS Turbo Module's Objective-C++ class implement to satisfy its Codegen-generated spec?
answer
- adopt the <Name>Spec protocol
- selectors copied from the generated header
- getTurboModule returns the SpecJSI
- +moduleName matches the JS name
- modulesProvider maps name to class
basics
~20 sThe class adopts the generated NativeXxxSpec protocol, implements every selector it declares, returns the generated NativeXxxSpecJSI from getTurboModule:, returns the JavaScript name from +moduleName, and is registered in codegenConfig.ios so React Native can map that name to the class.
solid answer
~30 sCodegen turns a spec like `NativeSmartLock.ts` into a protocol `NativeSmartLockSpec` (extending `RCTBridgeModule` and `RCTTurboModule`) and a C++ class `NativeSmartLockSpecJSI`. The `.mm` class must: adopt the protocol in its header by importing `<SmartLockSpec/SmartLockSpec.h>`; implement every declared selector exactly — a `Promise` method gains trailing `resolve:` and `reject:` blocks, a synchronous one returns its value; implement `getTurboModule:` returning `std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params)`; and implement `+moduleName` returning the same name JavaScript asks `TurboModuleRegistry` for. Finally, list it under `codegenConfig.ios` in `package.json` (`modulesProvider` in the tutorial) and re-run `pod install`. No `RCT_EXPORT_MODULE` or `RCT_EXPORT_METHOD` is involved.
code
objective-cpp · 43 lines// RCTSmartLock.h
#import <Foundation/Foundation.h>
#import <SmartLockSpec/SmartLockSpec.h>
@interface RCTSmartLock : NSObject <NativeSmartLockSpec>
@end
// RCTSmartLock.mm
#import "RCTSmartLock.h"
@implementation RCTSmartLock
+ (NSString *)moduleName
{
return @"NativeSmartLock";
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params);
}
// sdkVersion(): string
- (NSString *)sdkVersion
{
return @"2.4.0";
}
// unlock(lockId: string): Promise<void>
- (void)unlock:(NSString *)lockId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
if (lockId.length == 0) {
reject(@"E_BAD_ID", @"lockId must not be empty", nil);
return;
}
// ...call into the lock SDK here...
resolve(nil);
}
@endgo deeper
Recall the four pieces: adopt the generated protocol, implement its methods, write getTurboModule:, and return the module name, then register it in package.json.
Derive the selectors from a spec (sync return, Promise with resolve and reject), explain what the SpecJSI class is for, and say where protocol and header names come from.
Diagnose a module that compiles but cannot be found from JavaScript: name drift across +moduleName, the codegenConfig key and getEnforcing, or stale generated code.
Treat the generated protocol as the cross-language contract and decide how a team keeps spec, native code and registration in step across releases, for example in review and CI.
## The contract in one sentence A **Turbo Module** on iOS is an Objective-C++ class that **adopts a protocol Codegen wrote from your TypeScript spec**, hands React Native a **generated C++ JSI object** for itself, and is **registered by name** in `package.json`. Everything else — which thread it uses, how it emits events, how it is packaged — layers on top of that contract. ## What Codegen gives you to implement For a spec file `NativeSmartLock.ts` in a project whose `codegenConfig.name` is `SmartLockSpec`, running `pod install` makes Codegen produce `SmartLockSpec/SmartLockSpec.h`. The pieces your class touches: - **`NativeSmartLockSpec`** — an Objective-C protocol that extends `RCTBridgeModule` and `RCTTurboModule`, with one selector per spec method. The protocol is named after the **spec file**; the header is named after **`codegenConfig.name`**. - **`facebook::react::NativeSmartLockSpecJSI`** — a C++ class deriving from `ObjCTurboModule`. JavaScript holds this object; it converts arguments and forwards each call to your instance. - **`JS::` structs** for typed object parameters, when the spec uses them. You can jump to the protocol from Xcode, and Xcode can generate method stubs from it. ## The five things the class must do 1. **Adopt the protocol** in its header: `@interface RCTSmartLock : NSObject <NativeSmartLockSpec>`, after importing `<SmartLockSpec/SmartLockSpec.h>`. 2. **Implement every protocol selector** with the generated signature. The mapping is mechanical: | Spec method | Generated selector shape | |---|---| | `sdkVersion(): string` | `- (NSString *)sdkVersion;` — returns a value synchronously | | `unlock(lockId: string): Promise<void>` | `- (void)unlock:(NSString *)lockId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;` | | `lock(lockId: string): void` | `- (void)lock:(NSString *)lockId;` — fire and forget | 3. **Implement `getTurboModule:`**, taking `const facebook::react::ObjCTurboModule::InitParams &` and returning `std::make_shared<facebook::react::NativeSmartLockSpecJSI>(params)`. This is the factory through which React Native obtains the JSI wrapper for your instance, and it is why the file must be `.mm`. 4. **Implement `+moduleName`**, returning the JavaScript-facing name. `RCTBridgeModule` requires it, and it should equal the string the spec passes to `TurboModuleRegistry.getEnforcing`, here `'NativeSmartLock'`. 5. **Be registered.** Add the name-to-class mapping under `codegenConfig.ios` — the 0.87 tutorial uses `"modulesProvider": { "NativeSmartLock": "RCTSmartLock" }`, and the Codegen reference documents the equivalent per-module form `ios.modules.NativeSmartLock.className` — then re-run `pod install`. Codegen writes an `RCTModuleProviders.mm` that maps each name to its class, and the app's `RCTAppDependencyProvider` hands that map to React Native. ## What is not part of the contract any more - **`RCT_EXPORT_MODULE`** — the legacy registration macro. The module is found through the Codegen-generated provider map instead. - **`RCT_EXPORT_METHOD`** — the legacy way to expose a method. The method list now comes from the protocol, which comes from the spec. - **`NativeModules.SmartLock`** on the JavaScript side — replaced by the typed spec's default export. Since React Native 0.82 the New Architecture is the only architecture, so this protocol-plus-factory shape is the way to write an iOS module. Legacy modules still load through the interop layer, but they are not what a new module should look like. ## Where the pieces live on disk - **Your code:** `RCTSmartLock.h` and `RCTSmartLock.mm`, in an app group or a library's `ios/` folder. - **Generated code:** the `SmartLockSpec` header and its `-generated.mm` companion, rewritten by Codegen on every `pod install`; never edit them by hand, because the next install overwrites them. - **Registration:** `package.json`'s `codegenConfig`, read by Codegen at the same time. In an Expo project the same files appear after prebuild; the Expo Modules API is a different authoring route with its own contract. ## What goes wrong when a piece is missing - **Selector mismatch** — a hand-written signature that differs from the protocol (a renamed argument label, a missing `reject:`) produces a compiler warning about an unimplemented protocol method, and a call from JavaScript has no method to land on. - **Name mismatch** — if `+moduleName`, the `modulesProvider` key and the name in `getEnforcing` disagree, JavaScript throws `TurboModuleRegistry.getEnforcing(...): 'NativeSmartLock' could not be found`. - **Stale generated code** — editing the spec without re-running `pod install` leaves the old protocol in place, so new methods do not exist on the native side. - **Returning `nullptr` from `getTurboModule:`** — React Native logs an error and the module is unusable. ## How interviewers probe it They usually hand you a spec with one synchronous getter and one `Promise` method and ask for the `.mm`. A strong answer names the protocol, derives both selectors correctly, writes the factory, mentions `+moduleName` and registration, and says that `pod install` regenerates the header. Spec authoring, the threading of each call kind and event emitters are separate topics.
- Why does the protocol name differ from the generated header's name?They come from different inputs. The protocol and the `...SpecJSI` class are named after the spec file (`NativeSmartLock.ts` gives `NativeSmartLockSpec`), while the header and its folder are named after `codegenConfig.name` (`SmartLockSpec/SmartLockSpec.h`). One library can hold several specs, so several protocols share one header.
- What does React Native do with the name returned by +moduleName?It is the identity the module answers to. `RCTBridgeModule` requires the method, and it should match both the key under `codegenConfig.ios` and the string the spec passes to `TurboModuleRegistry.getEnforcing`. When they drift apart, the lookup from JavaScript fails with a could-not-be-found error even though the class compiles.
- How do you find the exact selectors to implement?Open the generated header after `pod install` — Xcode can jump to the `NativeSmartLockSpec` protocol and generate stubs. Do not guess: a `Promise` method gains `resolve:` and `reject:` labels, and numbers arrive as `double`, so a hand-written signature easily misses the protocol.
saying these in an interview costs you the question
- You still need RCT_EXPORT_METHOD on each method so JavaScript can see it
- The protocol and the header are both named after codegenConfig.name
- getTurboModule: can return self because the class already adopts the spec
- Codegen output updates by itself when you edit the spec, no pod install needed
- +moduleName is optional once the class is listed in modulesProvider