TurboModules and Fabric Components
How Codegen turns a TypeScript spec into a TurboModule or a Fabric view, and when to use it over Expo Modules.
TurboModuleRegistry.getEnforcing<Spec>(name: string): SpecTurboModules and Fabric components are React Native's own native interfaces, part of the New Architecture. They have been the default since 0.76, and in 0.87 TurboModules are mandatory: the useTurboModules feature flag was removed. The old asynchronous bridge is gone, so a module call reaches native code directly through JSI and a Function-style method can return a value synchronously.
Codegen
You do not hand-write the glue. You write a TypeScript spec and Codegen generates the C++, Objective-C and Java interfaces that your native class implements. The file name is the trigger: a module spec is Native*.ts, a view spec is *NativeComponent.ts. Codegen will not see a file named anything else.
package.json points at them through codegenConfig, with type set to "modules" or "components" and jsSrcsDir naming the folder. A module spec is an interface Spec extends TurboModule registered with TurboModuleRegistry.getEnforcing<Spec>(name). A view spec is a NativeProps interface extending ViewProps, passed to codegenNativeComponent.
Which to use
The Expo Modules API is less code for the same result and is the better default. Write a TurboModule directly when you are publishing a library for bare React Native users, or wrapping a native view where you need the generated prop diffing.
What trips people up: the spec types are a restricted subset. Unions of arbitrary types, generics and index signatures will fail Codegen rather than compile.
Example
// specs/NativeLocalStorage.ts
// The "Native" prefix is not a convention, it is how Codegen finds the file.
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}
// getEnforcing throws if the native side is missing.
// TurboModuleRegistry.get<Spec>(name) is the same lookup but returns null,
// which is what you want when the module is optional.
export default TurboModuleRegistry.getEnforcing<Spec>("NativeLocalStorage");
// ---------------------------------------------------------------
// Using it elsewhere in the app:
//
// import NativeLocalStorage from "@/specs/NativeLocalStorage";
//
// NativeLocalStorage.setItem("dark", "theme");
// console.log(NativeLocalStorage.getItem("theme"));
// Output: dark
//
// There is no await on getItem. Over JSI the call is synchronous, which the
// old bridge could not do - every bridge method returned a Promise.
// ---------------------------------------------------------------When to use it
- Publish a library that bare React Native projects can install without the Expo modules runtime.
- Wrap a platform view such as a map or video surface so it can be used as JSX.
- Replace a chatty async call with a synchronous JSI read when profiling shows the round trips dominate.
More examples
Wiring Codegen in package.json
Without this block Codegen has nothing to scan and your spec produces no interfaces, so the native class has nothing to implement. type is "modules" for TurboModules and "components" for Fabric views. modulesProvider maps the JavaScript name in getEnforcing to the Objective-C or Objective-C++ class that implements it on iOS; javaPackageName is where the generated Java lands on Android.
{
"name": "my-app",
"version": "1.0.0",
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativelocalstorage"
},
"ios": {
"modulesProvider": {
"NativeLocalStorage": "RCTNativeLocalStorage"
}
}
}
}A Fabric component spec
This is how a real native view becomes a JSX element. Extending ViewProps is what gives the component style, onLayout and the accessibility props for free. Reach for a Fabric component when you must render something the platform owns and JavaScript cannot redraw: a map, a video surface, a web view, a native camera preview. Note the prop types are deliberately plain — a union of string literals is allowed, an arbitrary union is not.
// specs/CustomWebViewNativeComponent.ts
// The NativeComponent suffix is what Codegen detects.
import type {
CodegenTypes,
HostComponent,
ViewProps,
} from "react-native";
import { codegenNativeComponent } from "react-native";
type WebViewScriptLoadedEvent = {
result: "success" | "error";
};
export interface NativeProps extends ViewProps {
sourceURL?: string;
// A bubbling event handler; the native side invokes it with the payload.
onScriptLoaded?: CodegenTypes.BubblingEventHandler<WebViewScriptLoadedEvent> | null;
}
export default codegenNativeComponent<NativeProps>(
"CustomWebView"
) as HostComponent<NativeProps>;Rendering the Fabric component
On screen: a full-height web view with a spinner over it until the native side reports back, replaced by an error line if the load failed. The element is used exactly like any other React Native component, which is the point of the generated HostComponent type. The style prop works because NativeProps extends ViewProps — but it is not CSS, and a native view with no height and no flex renders as a zero-pixel box rather than filling its parent.
// app/HelpScreen.tsx
import { useState } from "react";
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
import CustomWebView from "@/specs/CustomWebViewNativeComponent";
export default function HelpScreen() {
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
return (
<View style={styles.container}>
<CustomWebView
sourceURL="https://example.com/help"
// event.nativeEvent carries the payload typed in the spec.
onScriptLoaded={(event) => setStatus(event.nativeEvent.result)}
style={styles.web}
/>
{status === "loading" ? <ActivityIndicator /> : null}
{status === "error" ? <Text style={styles.error}>Could not load help.</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
// The native view has no intrinsic size, so it needs flex: 1 or a height.
web: { flex: 1 },
error: { padding: 16, color: "#b42318" },
});
Discussion