Introduction
React Native’s ability to bridge native code with JavaScript has been a game-changer for mobile development. With the introduction of TurboModules and JSI (JavaScript Interface), this bridge has become even more powerful and efficient. In this comprehensive guide, we’ll explore how to integrate custom native modules into an Expo-managed React Native application, focusing on the modern TurboModules approach.
Table of Contents
- Understanding the Expo Workflow
- When to Use Native Modules
- TurboModules vs Legacy NativeModules
- Setting Up Your Development Environment
- Creating a Custom Native Module
- Performance Considerations
- Best Practices and Conclusion
Understanding the Expo Workflow
Expo provides two main workflows for React Native development:
Managed Workflow
- Pre-built native code
- Limited access to native modules
- Easier development experience
- No direct access to native code
Bare Workflow
- Full access to native code
- Custom native module support
- More complex setup
- Greater flexibility
When to Use Native Modules
Scenario | Approach |
---|---|
Basic app functionality | Managed Workflow |
Custom native features | Bare Workflow |
Performance-critical features | Bare Workflow |
Third-party SDK integration | Bare Workflow |
TurboModules vs Legacy NativeModules
Performance Comparison
Feature | TurboModules | Legacy NativeModules |
---|---|---|
Initialization | Lazy loading | Eager loading |
Memory usage | Lower | Higher |
Type safety | Better | Limited |
Bridge overhead | Minimal | Significant |
Key Benefits of TurboModules
- Lazy Loading: Modules are loaded only when needed
- Direct Native Calls: JSI enables direct communication
- Type Safety: Better TypeScript integration
- Reduced Bridge Overhead: More efficient communication
Setting Up Your Development Environment
- Install the Expo CLI:
npm install -g expo-cli
- Create a new Expo project:
expo init MyNativeApp
cd MyNativeApp
- Eject to bare workflow:
expo prebuild
Creating a Custom Native Module
Let’s create a simple native module that performs a CPU-intensive calculation. We’ll implement this in Kotlin for Android.
1. Create the Native Module Interface
// android/app/src/main/java/com/mynativeapp/NativeCalculatorModule.kt
package com.mynativeapp
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
class NativeCalculatorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "NativeCalculator"
@ReactMethod
fun calculateFibonacci(n: Int, promise: Promise) {
try {
val result = calculateFibonacciNative(n)
promise.resolve(result)
} catch (e: Exception) {
promise.reject("CALCULATION_ERROR", e.message)
}
}
private fun calculateFibonacciNative(n: Int): Int {
if (n <= 1) return n
var a = 0
var b = 1
for (i in 2..n) {
val temp = a + b
a = b
b = temp
}
return b
}
}
2. Create the Package
// android/app/src/main/java/com/mynativeapp/NativeCalculatorPackage.kt
package com.mynativeapp
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class NativeCalculatorPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(NativeCalculatorModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
3. Register the Package
// android/app/src/main/java/com/mynativeapp/MainApplication.kt
// ... existing imports ...
class MainApplication : Application(), ReactApplication {
private val mReactNativeHost = object : ReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
packages.add(NativeCalculatorPackage())
return packages
}
// ... rest of the implementation
}
}
4. Using the Module in JavaScript
// App.tsx
import { NativeModules } from 'react-native';
const { NativeCalculator } = NativeModules;
const App = () => {
const calculateFibonacci = async (n: number) => {
try {
const result = await NativeCalculator.calculateFibonacci(n);
console.log(`Fibonacci(${n}) = ${result}`);
} catch (error) {
console.error('Calculation failed:', error);
}
};
return (
// ... your component JSX
);
};
Performance Considerations
Space Complexity
- TurboModules: O(1) for module initialization
- Legacy NativeModules: O(n) where n is the number of modules
Time Complexity
- TurboModules: O(1) for method calls
- Legacy NativeModules: O(1) but with higher constant factors due to bridge overhead
Best Practices and Conclusion
Best Practices
- Use TurboModules for new native module development
- Implement proper error handling
- Consider type safety with Codegen
- Profile performance before and after native module integration
- Document your native module API thoroughly
When to Use This Approach
- Performance-critical calculations
- Complex native functionality
- Third-party SDK integration
- Hardware-specific features
Future Considerations
For even better type safety and developer experience, consider implementing Codegen for your native modules. This will be covered in a follow-up post, where we’ll explore how to generate type-safe interfaces automatically.
Additional Resources
Note: This blog post assumes basic knowledge of React Native and Android development. For more detailed information about specific topics, please refer to the official documentation.