permissions
swift
native-modules
biometrics
react native
bridging
turbomodules
camera
kotlin
jsi
React Native CLI
If you come from native mobile development, you already know that the true power of an app lies in accessing hardware: camera, sensors, biometrics, secure storage. In React Native, this works through bridging: connecting your JavaScript with device native code.
When you hear "bridging" or "creating a bridge," we're really talking about creating a Native Module: a piece of native code (Kotlin/Swift) that you expose to JavaScript. You're not creating the bridge itself - that communication mechanism already exists in React Native. What you do is:
Two ways to do bridging:
| Method | What does it use? | Status |
|---|---|---|
| Native Modules (traditional) | Bridge (JSON) | Works, but slower |
| TurboModules (new) | JSI (direct) | Faster, on-demand loading |
Both achieve the same thing, connecting JS with native code. The difference is the internal communication mechanism.
In this article you'll see how to use device APIs and, when necessary, create your own bridges (native modules). Think of this as the equivalent of when in Android you write Kotlin code that interacts with the system, or in iOS when you use Swift to access native frameworks.
In mobile, any access to sensitive resources needs explicit user permissions. You can't just access the camera without asking first. React Native handles this similar to how you do it in Android or iOS, but with a unified API. Let's use react-native-permissions, the standard library for this:
1npm install react-native-permissions 2cd ios && pod install
Open the ios/YourApp/Info.plist file (where "YourApp" is your project name) and add the descriptions the user will see when you request permissions:
1<key>NSCameraUsageDescription</key> 2<string>We need access to your camera to take photos</string> 3 4<key>NSPhotoLibraryUsageDescription</key> 5<string>We need access to your photos to select images</string> 6 7<key>NSMicrophoneUsageDescription</key> 8<string>We need access to the microphone to record audio</string> 9 10<key>NSLocationWhenInUseUsageDescription</key> 11<string>We need your location to show you nearby content</string> 12 13<key>NSFaceIDUsageDescription</key> 14<string>Use Face ID for secure access</string>
Open the android/app/src/main/AndroidManifest.xml file and declare the permissions you'll need:
1<manifest> 2 <uses-permission android:name="android.permission.CAMERA" /> 3 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 4 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 5 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 6 <uses-permission android:name="android.permission.RECORD_AUDIO" /> 7 <uses-permission android:name="android.permission.USE_BIOMETRIC" /> 8</manifest>
Now comes the interesting part. Asking the user for permission at the right moment. Create a src/utils/permissions.js file (or wherever you have your utilities):
1import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; 2import { Platform } from 'react-native'; 3 4async function requestCameraPermission() { 5 // Select permission based on platform 6 const permission = Platform.select({ 7 ios: PERMISSIONS.IOS.CAMERA, 8 android: PERMISSIONS.ANDROID.CAMERA, 9 }); 10 11 // First check current status 12 const result = await check(permission); 13 14 switch (result) { 15 case RESULTS.UNAVAILABLE: 16 console.log('This feature is not available on this device'); 17 return false; 18 19 case RESULTS.DENIED: 20 console.log('Permission denied, let\'s request it...'); 21 const requestResult = await request(permission); 22 return requestResult === RESULTS.GRANTED; 23 24 case RESULTS.LIMITED: 25 console.log('Limited permission (iOS 14+ only)'); 26 return true; 27 28 case RESULTS.GRANTED: 29 console.log('You already have permission, all good'); 30 return true; 31 32 case RESULTS.BLOCKED: 33 console.log('Permission is blocked, user must go to Settings'); 34 // Here you could show a dialog to go to Settings 35 return false; 36 } 37}
If you come from native, this will sound familiar:
In Kotlin/Android:
1if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) 2 != PackageManager.PERMISSION_GRANTED) { 3 ActivityCompat.requestPermissions(activity, 4 arrayOf(Manifest.permission.CAMERA), REQUEST_CODE) 5}
In Swift/iOS:
1AVCaptureDevice.requestAccess(for: .video) { granted in 2 if granted { 3 // Permission granted 4 } 5}
The abstraction is great: one API works on both platforms.
Let's use react-native-vision-camera, which is the most modern and performant library for working with the camera. Forget about the old react-native-camera, Vision Camera is the new standard.
1npm install react-native-vision-camera 2cd ios && pod install
You know the drill. First the texts the user will see:
iOS: In your ios/YourApp/Info.plist:
1<key>NSCameraUsageDescription</key> 2<string>We need the camera to take photos</string>
Android: In your android/app/src/main/AndroidManifest.xml:
1<uses-permission android:name="android.permission.CAMERA" /> 2<uses-feature android:name="android.hardware.camera" android:required="false" />
Now comes the good part. Let's make a functional camera in less than 100 lines. Create the src/screens/CameraScreen.js file (or .tsx if you use TypeScript):
1import React, { useRef, useState } from 'react'; 2import { View, TouchableOpacity, Text, StyleSheet, Image } from 'react-native'; 3import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera'; 4 5export default function CameraScreen() { 6 const camera = useRef(null); 7 const device = useCameraDevice('back'); 8 const { hasPermission, requestPermission } = useCameraPermission(); 9 const [photo, setPhoto] = useState(null); 10 11 // Request permission if we don't have it 12 if (!hasPermission) { 13 return ( 14 <View style={styles.container}> 15 <Text>We need access to your camera</Text> 16 <TouchableOpacity style={styles.button} onPress={requestPermission}> 17 <Text style={styles.buttonText}>Grant Permission</Text> 18 </TouchableOpacity> 19 </View> 20 ); 21 } 22 23 // Check that there's an available camera 24 if (!device) { 25 return ( 26 <View style={styles.container}> 27 <Text>No camera found</Text> 28 </View> 29 ); 30 } 31 32 const takePhoto = async () => { 33 if (camera.current) { 34 const photo = await camera.current.takePhoto({ 35 qualityPrioritization: 'balanced', 36 flash: 'auto', 37 }); 38 39 setPhoto(photo); 40 console.log('Photo taken:', photo.path); 41 } 42 }; 43 44 // If we already took a photo, show it 45 if (photo) { 46 return ( 47 <View style={styles.container}> 48 <Image 49 source={{ uri: `file://${photo.path}` }} 50 style={styles.preview} 51 /> 52 <TouchableOpacity 53 style={styles.button} 54 onPress={() => setPhoto(null)} 55 > 56 <Text style={styles.buttonText}>Take another</Text> 57 </TouchableOpacity> 58 </View> 59 ); 60 } 61 62 return ( 63 <View style={styles.container}> 64 <Camera 65 ref={camera} 66 style={StyleSheet.absoluteFill} 67 device={device} 68 isActive={true} 69 photo={true} 70 /> 71 72 <View style={styles.controls}> 73 <TouchableOpacity style={styles.captureButton} onPress={takePhoto}> 74 <View style={styles.captureButtonInner} /> 75 </TouchableOpacity> 76 </View> 77 </View> 78 ); 79} 80 81const styles = StyleSheet.create({ 82 container: { 83 flex: 1, 84 justifyContent: 'center', 85 alignItems: 'center', 86 backgroundColor: 'black', 87 }, 88 controls: { 89 position: 'absolute', 90 bottom: 50, 91 alignSelf: 'center', 92 }, 93 captureButton: { 94 width: 70, 95 height: 70, 96 borderRadius: 35, 97 backgroundColor: 'white', 98 justifyContent: 'center', 99 alignItems: 'center', 100 }, 101 captureButtonInner: { 102 width: 60, 103 height: 60, 104 borderRadius: 30, 105 backgroundColor: 'white', 106 borderWidth: 2, 107 borderColor: 'black', 108 }, 109 preview: { 110 width: '100%', 111 height: '80%', 112 }, 113 button: { 114 backgroundColor: '#2196F3', 115 padding: 15, 116 borderRadius: 8, 117 marginTop: 20, 118 }, 119 buttonText: { 120 color: 'white', 121 fontSize: 16, 122 fontWeight: '600', 123 }, 124});
What's happening here:
useCameraPermission() - Manages permission state automaticallyuseCameraDevice('back') - Selects the back camera (you can use 'front' too)camera.current - Reference to control the camera programmaticallytakePhoto() - Returns a promise with the file path where the photo was savedIf you've used AVFoundation on iOS or Camera2 API on Android, this will feel very natural. The difference is that here you write once and it works on both platforms.
Face ID, Touch ID, fingerprint... biometrics is standard nowadays. Let's implement it.
1npm install react-native-biometrics 2cd ios && pod install
First you need to know if the device supports biometrics and what type. Create a src/utils/biometrics.js file:
1import ReactNativeBiometrics from 'react-native-biometrics'; 2 3async function checkBiometrics() { 4 const rnBiometrics = new ReactNativeBiometrics(); 5 6 const { available, biometryType } = await rnBiometrics.isSensorAvailable(); 7 8 if (available) { 9 switch (biometryType) { 10 case 'FaceID': 11 console.log('Face ID available'); 12 break; 13 case 'TouchID': 14 console.log('Touch ID available'); 15 break; 16 case 'Biometrics': 17 console.log('Biometrics available (Android)'); 18 break; 19 } 20 } else { 21 console.log('Biometrics not available'); 22 } 23 24 return available; 25}
Now yes, let's ask the user to authenticate. Create src/screens/BiometricLoginScreen.js:
1import React, { useState } from 'react'; 2import { View, TouchableOpacity, Text, Alert, StyleSheet } from 'react-native'; 3import ReactNativeBiometrics from 'react-native-biometrics'; 4 5export default function BiometricLogin() { 6 const [isAuthenticated, setIsAuthenticated] = useState(false); 7 8 const authenticate = async () => { 9 try { 10 const rnBiometrics = new ReactNativeBiometrics(); 11 12 // Check if biometrics is available 13 const { available } = await rnBiometrics.isSensorAvailable(); 14 15 if (!available) { 16 Alert.alert('Error', 'Biometrics not available on this device'); 17 return; 18 } 19 20 // Request authentication 21 const { success } = await rnBiometrics.simplePrompt({ 22 promptMessage: 'Authenticate to continue', 23 cancelButtonText: 'Cancel', 24 }); 25 26 if (success) { 27 setIsAuthenticated(true); 28 Alert.alert('Success', 'Authentication successful!'); 29 } else { 30 Alert.alert('Error', 'Authentication cancelled or failed'); 31 } 32 } catch (error) { 33 console.error('Authentication error:', error); 34 Alert.alert('Error', 'There was a problem with authentication'); 35 } 36 }; 37 38 if (isAuthenticated) { 39 return ( 40 <View style={styles.container}> 41 <Text style={styles.title}>✓ Authenticated</Text> 42 <Text style={styles.subtitle}>Welcome back</Text> 43 <TouchableOpacity 44 style={styles.button} 45 onPress={() => setIsAuthenticated(false)} 46 > 47 <Text style={styles.buttonText}>Logout</Text> 48 </TouchableOpacity> 49 </View> 50 ); 51 } 52 53 return ( 54 <View style={styles.container}> 55 <Text style={styles.title}>🔒 Login</Text> 56 <Text style={styles.subtitle}>Use biometrics to access</Text> 57 58 <TouchableOpacity style={styles.button} onPress={authenticate}> 59 <Text style={styles.buttonText}>Authenticate</Text> 60 </TouchableOpacity> 61 </View> 62 ); 63} 64 65const styles = StyleSheet.create({ 66 container: { 67 flex: 1, 68 justifyContent: 'center', 69 alignItems: 'center', 70 padding: 20, 71 backgroundColor: '#f5f5f5', 72 }, 73 title: { 74 fontSize: 32, 75 fontWeight: 'bold', 76 marginBottom: 10, 77 }, 78 subtitle: { 79 fontSize: 16, 80 color: '#666', 81 marginBottom: 40, 82 }, 83 button: { 84 backgroundColor: '#2196F3', 85 paddingHorizontal: 40, 86 paddingVertical: 15, 87 borderRadius: 8, 88 }, 89 buttonText: { 90 color: 'white', 91 fontSize: 18, 92 fontWeight: '600', 93 }, 94});
Never, but NEVER, save passwords or tokens in AsyncStorage. That's plain text. For sensitive data, use the keychain (iOS) and Keystore (Android).
1npm install react-native-keychain 2cd ios && pod install
This is how you save data securely. Create src/utils/secureStorage.js:
1import * as Keychain from 'react-native-keychain'; 2 3// Save credentials 4async function saveCredentials(username, password) { 5 try { 6 await Keychain.setGenericPassword(username, password, { 7 service: 'com.myapp.auth', 8 accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, 9 }); 10 console.log('Credentials saved securely'); 11 return true; 12 } catch (error) { 13 console.error('Error saving:', error); 14 return false; 15 } 16} 17 18// Retrieve credentials 19async function getCredentials() { 20 try { 21 const credentials = await Keychain.getGenericPassword({ 22 service: 'com.myapp.auth', 23 }); 24 25 if (credentials) { 26 console.log('Username:', credentials.username); 27 console.log('Password:', credentials.password); 28 return credentials; 29 } else { 30 console.log('No saved credentials'); 31 return null; 32 } 33 } catch (error) { 34 console.error('Error retrieving:', error); 35 return null; 36 } 37} 38 39// Delete credentials 40async function deleteCredentials() { 41 try { 42 await Keychain.resetGenericPassword({ 43 service: 'com.myapp.auth', 44 }); 45 console.log('Credentials deleted'); 46 return true; 47 } catch (error) { 48 console.error('Error deleting:', error); 49 return false; 50 } 51}
Let's put everything together in a functional login that persists credentials. Create src/screens/SecureLoginScreen.js:
1import React, { useState, useEffect } from 'react'; 2import { View, TextInput, TouchableOpacity, Text, StyleSheet, Alert } from 'react-native'; 3import * as Keychain from 'react-native-keychain'; 4 5export default function SecureLogin() { 6 const [username, setUsername] = useState(''); 7 const [password, setPassword] = useState(''); 8 const [isLoggedIn, setIsLoggedIn] = useState(false); 9 10 // On mount, check for stored credentials 11 useEffect(() => { 12 checkStoredCredentials(); 13 }, []); 14 15 const checkStoredCredentials = async () => { 16 try { 17 const credentials = await Keychain.getGenericPassword(); 18 if (credentials) { 19 setUsername(credentials.username); 20 setIsLoggedIn(true); 21 } 22 } catch (error) { 23 console.error('Error checking credentials:', error); 24 } 25 }; 26 27 const handleLogin = async () => { 28 if (!username || !password) { 29 Alert.alert('Error', 'Please fill in all fields'); 30 return; 31 } 32 33 // Here you would make your API call 34 // For now, we simulate a successful login 35 36 try { 37 await Keychain.setGenericPassword(username, password); 38 setIsLoggedIn(true); 39 Alert.alert('Success', 'Credentials saved securely'); 40 } catch (error) { 41 Alert.alert('Error', 'Could not save credentials'); 42 } 43 }; 44 45 const handleLogout = async () => { 46 try { 47 await Keychain.resetGenericPassword(); 48 setIsLoggedIn(false); 49 setUsername(''); 50 setPassword(''); 51 Alert.alert('Session closed', 'Credentials deleted'); 52 } catch (error) { 53 Alert.alert('Error', 'Could not logout'); 54 } 55 }; 56 57 if (isLoggedIn) { 58 return ( 59 <View style={styles.container}> 60 <Text style={styles.title}>Welcome, {username}!</Text> 61 <TouchableOpacity style={styles.button} onPress={handleLogout}> 62 <Text style={styles.buttonText}>Logout</Text> 63 </TouchableOpacity> 64 </View> 65 ); 66 } 67 68 return ( 69 <View style={styles.container}> 70 <Text style={styles.title}>Secure Login</Text> 71 72 <TextInput 73 style={styles.input} 74 placeholder="Username" 75 value={username} 76 onChangeText={setUsername} 77 autoCapitalize="none" 78 /> 79 80 <TextInput 81 style={styles.input} 82 placeholder="Password" 83 value={password} 84 onChangeText={setPassword} 85 secureTextEntry 86 /> 87 88 <TouchableOpacity style={styles.button} onPress={handleLogin}> 89 <Text style={styles.buttonText}>Login</Text> 90 </TouchableOpacity> 91 </View> 92 ); 93} 94 95const styles = StyleSheet.create({ 96 container: { 97 flex: 1, 98 justifyContent: 'center', 99 padding: 20, 100 backgroundColor: '#f5f5f5', 101 }, 102 title: { 103 fontSize: 24, 104 fontWeight: 'bold', 105 marginBottom: 30, 106 textAlign: 'center', 107 }, 108 input: { 109 backgroundColor: 'white', 110 padding: 15, 111 borderRadius: 8, 112 marginBottom: 15, 113 borderWidth: 1, 114 borderColor: '#ddd', 115 }, 116 button: { 117 backgroundColor: '#2196F3', 118 padding: 15, 119 borderRadius: 8, 120 alignItems: 'center', 121 }, 122 buttonText: { 123 color: 'white', 124 fontSize: 16, 125 fontWeight: '600', 126 }, 127});
Here comes the interesting part. React Native works with two "worlds" that communicate:
┌─────────────────┐ ┌──────────────────┐
│ JavaScript │ Bridging│ Native Code │
│ (Your code) │ ←─────→ │ (Kotlin/Swift) │
└─────────────────┘ └──────────────────┘
That "bridging" (connection) can work in two ways:
It's the original communication mechanism:
1// Traditional Native Module 2NativeModules.MyModule.doSomething(); // Always asynchronous
The new architecture changes the game, because it has synchronous communication when you need it, no unnecessary JSON serialization, modules load on demand (lazy loading) and it's much, much faster.
1// TurboModule 2const result = TurboModules.MyModule.doSomething(); // Can be synchronous!
In summary:
Both achieve the same thing, connecting JS with native code. The difference is the internal mechanism.
JSI (JavaScript Interface) is the layer that allows JavaScript and C++ to talk directly, without the JSON bridge in between. It replaces the traditional Bridge. What it gives you is you can make synchronous calls (when you need them), be without serialization/deserialization overhead, have direct access to native objects from JS and significantly better performance.
They are native modules built on JSI (not on the Bridge). If you've ever created a Native Module, TurboModules are basically the same but better.
The key differences:
| Aspect | Native Modules (old) | TurboModules (new) |
|---|---|---|
| Loading | All at startup | On demand |
| Call types | Only asynchronous | Synchronous and asynchronous |
| Performance | JSON Bridge | Direct JSI |
| Type definition | Manual | Automatic (Codegen) |
In practice, traditional Native Modules still work perfectly. But if you're starting a new project or creating a library, it's worth using TurboModules.
Let's create something simple but useful: a module that gives you device information. This will teach you the complete flow of bridging between JS and native code.
File structure we'll create:
your-project/
├── specs/
│ └── NativeDeviceInfo.ts # TypeScript definition
├── ios/
│ ├── DeviceInfo.swift # iOS implementation
│ └── DeviceInfo.m # Objective-C bridge
└── android/app/src/main/java/com/yourapp/
├── DeviceInfoModule.kt # Android implementation
└── DeviceInfoPackage.kt # Module registration
First you tell React Native what methods your module will have.
Create the specs/NativeDeviceInfo.ts file in your project root:
1// specs/NativeDeviceInfo.ts 2import type { TurboModule } from 'react-native'; 3import { TurboModuleRegistry } from 'react-native'; 4 5export interface Spec extends TurboModule { 6 getDeviceModel(): string; 7 getBatteryLevel(): Promise<number>; 8 vibrate(duration: number): void; 9} 10 11export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');
Now let's go with the native code for iOS.
Create the ios/DeviceInfo.swift file:
1// ios/DeviceInfo.swift 2import Foundation 3import UIKit 4 5@objc(DeviceInfo) 6class DeviceInfo: NSObject { 7 8 @objc 9 func getDeviceModel() -> String { 10 return UIDevice.current.model 11 } 12 13 @objc 14 func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock, 15 rejecter reject: @escaping RCTPromiseRejectBlock) { 16 UIDevice.current.isBatteryMonitoringEnabled = true 17 let batteryLevel = UIDevice.current.batteryLevel 18 19 if batteryLevel < 0 { 20 reject("ERROR", "Could not get battery level", nil) 21 } else { 22 resolve(Double(batteryLevel * 100)) 23 } 24 } 25 26 @objc 27 func vibrate(_ duration: NSNumber) { 28 // iOS only supports fixed vibration 29 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) 30 } 31 32 @objc 33 static func requiresMainQueueSetup() -> Bool { 34 return false 35 } 36}
Now create the Objective-C bridge in ios/DeviceInfo.m:
1// ios/DeviceInfo.m 2#import <React/RCTBridgeModule.h> 3 4@interface RCT_EXTERN_MODULE(DeviceInfo, NSObject) 5 6RCT_EXTERN_METHOD(getDeviceModel) 7RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve 8 rejecter:(RCTPromiseRejectBlock)reject) 9RCT_EXTERN_METHOD(vibrate:(nonnull NSNumber *)duration) 10 11@end
And now the code for Android.
Create the android/app/src/main/java/com/yourapp/DeviceInfoModule.kt file (replace "yourapp" with your app name):
1// android/app/src/main/java/com/yourapp/DeviceInfoModule.kt 2package com.yourapp 3 4import android.os.Build 5import android.os.VibrationEffect 6import android.os.Vibrator 7import android.content.Context 8import android.content.Intent 9import android.content.IntentFilter 10import android.os.BatteryManager 11import com.facebook.react.bridge.ReactApplicationContext 12import com.facebook.react.bridge.ReactContextBaseJavaModule 13import com.facebook.react.bridge.ReactMethod 14import com.facebook.react.bridge.Promise 15 16class DeviceInfoModule(reactContext: ReactApplicationContext) : 17 ReactContextBaseJavaModule(reactContext) { 18 19 override fun getName(): String { 20 return "DeviceInfo" 21 } 22 23 @ReactMethod 24 fun getDeviceModel(): String { 25 return Build.MODEL 26 } 27 28 @ReactMethod 29 fun getBatteryLevel(promise: Promise) { 30 try { 31 val batteryStatus: Intent? = reactApplicationContext.registerReceiver( 32 null, 33 IntentFilter(Intent.ACTION_BATTERY_CHANGED) 34 ) 35 36 val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 37 val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 38 39 val batteryPct = level * 100 / scale.toFloat() 40 promise.resolve(batteryPct.toDouble()) 41 } catch (e: Exception) { 42 promise.reject("ERROR", "Could not get battery level", e) 43 } 44 } 45 46 @ReactMethod 47 fun vibrate(duration: Double) { 48 val vibrator = reactApplicationContext 49 .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 50 51 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 52 vibrator.vibrate( 53 VibrationEffect.createOneShot( 54 duration.toLong(), 55 VibrationEffect.DEFAULT_AMPLITUDE 56 ) 57 ) 58 } else { 59 @Suppress("DEPRECATION") 60 vibrator.vibrate(duration.toLong()) 61 } 62 } 63}
Now create the Package in android/app/src/main/java/com/yourapp/DeviceInfoPackage.kt:
1// android/app/src/main/java/com/yourapp/DeviceInfoPackage.kt 2package com.yourapp 3 4import com.facebook.react.ReactPackage 5import com.facebook.react.bridge.NativeModule 6import com.facebook.react.bridge.ReactApplicationContext 7import com.facebook.react.uimanager.ViewManager 8 9class DeviceInfoPackage : ReactPackage { 10 override fun createNativeModules(reactContext: ReactApplicationContext): 11 List<NativeModule> { 12 return listOf(DeviceInfoModule(reactContext)) 13 } 14 15 override fun createViewManagers(reactContext: ReactApplicationContext): 16 List<ViewManager<*, *>> { 17 return emptyList() 18 } 19}
Don't forget to register the package in android/app/src/main/java/com/yourapp/MainApplication.kt:
1override fun getPackages(): List<ReactPackage> { 2 return PackageList(this).packages.apply { 3 add(DeviceInfoPackage()) 4 } 5}
And now the magic. From your JavaScript you can call that native code.
Create src/screens/DeviceInfoScreen.js:
1import { NativeModules } from 'react-native'; 2const { DeviceInfo } = NativeModules; 3 4export default function DeviceInfoScreen() { 5 const [deviceModel, setDeviceModel] = useState(''); 6 const [batteryLevel, setBatteryLevel] = useState(0); 7 8 useEffect(() => { 9 loadDeviceInfo(); 10 }, []); 11 12 const loadDeviceInfo = async () => { 13 // Synchronous method 14 const model = DeviceInfo.getDeviceModel(); 15 setDeviceModel(model); 16 17 // Asynchronous method (Promise) 18 const battery = await DeviceInfo.getBatteryLevel(); 19 setBatteryLevel(battery); 20 }; 21 22 const handleVibrate = () => { 23 // Void method (no return) 24 DeviceInfo.vibrate(500); // 500ms 25 }; 26 27 return ( 28 <View style={styles.container}> 29 <Text style={styles.title}>Device Information</Text> 30 31 <View style={styles.info}> 32 <Text style={styles.label}>Model:</Text> 33 <Text style={styles.value}>{deviceModel}</Text> 34 </View> 35 36 <View style={styles.info}> 37 <Text style={styles.label}>Battery:</Text> 38 <Text style={styles.value}>{batteryLevel.toFixed(0)}%</Text> 39 </View> 40 41 <TouchableOpacity style={styles.button} onPress={handleVibrate}> 42 <Text style={styles.buttonText}>Vibrate</Text> 43 </TouchableOpacity> 44 </View> 45 ); 46}
Don't assume everything will work:
1try { 2 const result = await NativeModule.someMethod(); 3} catch (error) { 4 console.error('Error in native module:', error); 5 // Handle the error in the UI 6}
Not all devices have all capabilities:
1import { Platform, NativeModules } from 'react-native'; 2 3function isFeatureAvailable() { 4 if (Platform.OS === 'ios') { 5 return Platform.Version >= 13; // iOS 13+ 6 } else { 7 return Platform.Version >= 23; // Android 6.0+ 8 } 9}
Define the types of your native module to avoid errors:
1interface DeviceInfoModule { 2 getDeviceModel(): string; 3 getBatteryLevel(): Promise<number>; 4 vibrate(duration: number): void; 5} 6 7const DeviceInfo = NativeModules.DeviceInfo as DeviceInfoModule;
With these tools you can build apps that feel as native as if you had written them in pure Kotlin or Swift. The difference is that with React Native you write once and it works on both platforms. And yes, when you need extreme performance or access to very specific APIs, you can still write native code. React Native doesn't limit you, it gives you options.