Bridging Swift and Kotlin: When You Need to Go Native
Sometimes the JavaScript layer isn't enough. Here's when to reach for native code, and how to bridge it cleanly with TurboModules without painting yourself into a corner.
Most React Native apps never need to write a single line of Swift or Kotlin. The community has built libraries for almost everything: camera, location, sensors, payments, biometrics, file system. The third-party ecosystem is the reason most of us chose React Native in the first place.
But sometimes, the library you need doesn't exist. Or it exists but doesn't do the one thing your app needs. Or it works great on iOS but is broken on Android. That's when you reach for native code.
When bridging makes sense
A few signals that it's time to bridge:
You need to access a platform API that has no React Native library. This is rare but happens — I had to bridge to a custom biometric SDK once because the official one didn't support a specific sensor that the partner required.
You need performance that JavaScript can't deliver. Image processing, video manipulation, complex animations with hundreds of objects. These can run on the JS thread, but they shouldn't.
You have an existing native library or SDK and need to wrap it. The classic case is integrating with a partner's iOS/Android SDK that you don't control. You don't get to choose the platform.
The new way: TurboModules
The old way of writing native modules involved RCTBridgeModule and a lot of boilerplate. The new way, TurboModules, is dramatically better.
You define a TypeScript spec for your module. The build process generates the native interfaces for iOS and Android. You implement the methods in Swift and Kotlin. TypeScript verifies everything at build time, so you catch errors before the app runs.
The spec looks like this:
typescriptimport { TurboModule, TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { readSensor(name: string): Promise<number>; startListening(name: string): void; } export default TurboModuleRegistry.getEnforcing<Spec>('SensorModule');
The implementation on iOS is a Swift class that conforms to the generated protocol. The implementation on Android is a Kotlin class. Both expose the same methods, with the same types, defined in one place. No more guessing what shape the data is on the other side.
The gotchas
The biggest gotcha is that TurboModules are still being polished. Some edge cases don't have great error messages. If something doesn't work, you're often staring at a native crash with no obvious cause. Budget extra time for debugging.
The second gotcha is testing. You can write unit tests for the native code, but integration tests are harder. You'll need a real device or simulator to verify everything works end-to-end. Detox and Maestro can drive native modules, but the setup is heavier than pure JS testing.
The third gotcha is memory. Native modules can hold references to JS objects, and if you're not careful, you'll leak memory. The rule is: anything that comes in from JS gets released when the function returns. Anything that needs to be stored (like a callback or event listener) needs to be released explicitly.
When to use a library instead
If you can find a well-maintained library that does what you need, use it. Writing a native module is 2-4 days of work, and you'll be responsible for maintaining it across React Native versions.
If you're bridging to access a custom SDK or platform API, you don't have a choice. But if you're bridging because no library exists for a common use case, consider writing the library and publishing it. The community will thank you, and you'll save yourself the maintenance burden of a private module.
The bottom line
TurboModules are a powerful tool, but they're a tool of last resort. Most apps will never need to bridge. The few that do will spend more time on native code than they expect. Plan accordingly, and don't bridge until you've exhausted the library ecosystem.