How Custom Native Modules Work in React Native
How Custom Native Modules Work
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
This chapter does NOT write a custom native module (that would require Xcode/Android Studio and the "bare workflow"/expo prebuild, beyond the scope of this tutorial) – it explains CONCEPTUALLY what happens when a library like expo-camera or @react-native-async-storage/async-storage connects JavaScript to native Swift/Kotlin code, so you can READ and MAKE SENSE of someone else's native module code.
When do you even need your own native module?
Almost everything a typical app needs (camera, location, storage, notifications) already exists as a ready-made Expo/community library – useCart/cartStore in our project, for instance, NEVER touch native stuff directly, they use the ready-made AsyncStorage library. You only need YOUR OWN native module for: (1) platform-specific SDKs without an existing JS wrapper (e.g. a company's own payment SDK), (2) extremely performance-critical code better placed directly in Swift/Kotlin/C++, or (3) access to native behavior React Native itself doesn't expose.
The TurboModule specification
In the new architecture (chapter 8), a native module starts with a TypeScript "spec" file – this is EXACTLY the file you're most likely to encounter in someone else's code, even if you never write a module yourself:
// NativeMyModule.ts - purely declarative, no implementation code
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getBatteryLevel(): number;
showToast(message: string): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');From this TypeScript file, React Native AUTOMATICALLY generates C++ code at build time that bridges to the native implementation – you write the interface ONCE in TypeScript, and the native code on both platforms must conform to it exactly.
The native side: conceptual, not meant to be typed out
On iOS (Swift) and Android (Kotlin), one class each implements the methods defined in the spec – JSI then calls these methods DIRECTLY, without the old JSON bridge. The return type getBatteryLevel(): number in the spec means CONCRETELY: the native Swift/Kotlin implementation MUST be able to return a number synchronously – unlike the old bridge system, where practically EVERYTHING had to go through a promise/callback.
Back to our project: why AsyncStorage is fast
Now we can explain WHY AsyncStorage.getItem(...) from chapter 1/2 is performant, even though it calls native filesystem code: the modern version of the library implements EXACTLY this TurboModule pattern – a TypeScript spec file, native Swift/Kotlin implementations, connected via generated C++ code through JSI. As users of the library, we see NONE of this beyond the perfectly ordinary getItem/setItem API – that's exactly the point of a good abstraction.
Reading someone else's native module code: what to look for
- A
*.tsfile withextends TurboModule– that IS the public API contract, independent of the implementation. - An
ios/folder with.swift/.mfiles and anandroid/folder with.kt/.javafiles – the actual, platform-specific implementations. - A
*.podspecfile (iOS) orbuild.gradle(Android) – the build configuration telling the respective native build system how to compile the module.
Tipp: The practical value of this chapter for your day-to-day work: when a community library doesn't behave as expected, you can now search its GitHub repository specifically for the Spec file to understand WHICH methods/values are actually exchanged between JS and the native side – often more informative than the plain README documentation, especially for rarely maintained packages.