Writing Native Modules (Android & iOS)

📖 Concept

Native modules let you write platform-specific code (Kotlin/Java for Android, Swift/ObjC for iOS) and call it from JavaScript. This is necessary when:

  • No existing JS/RN library covers your need
  • You need to access platform APIs not exposed by React Native
  • Performance-critical code that must run natively (image processing, encryption)
  • Integrating third-party native SDKs

Old architecture (Bridge-based) native module:

  • Register module with the bridge
  • Methods are async-only (return via Promise or callback)
  • Data crosses the bridge as JSON — serialization overhead

New architecture (TurboModules):

  • Define TypeScript spec → codegen creates native interface
  • Supports synchronous methods (via JSI)
  • Lazy-loaded — only initialized when first used
  • Type-safe across the JS-Native boundary

When to write native vs stay in JS:

Need Approach
File system access Native module (or expo-file-system)
Bluetooth Native module (platform-specific APIs)
Image manipulation Native module (hardware-accelerated)
Encryption Native module (platform keychain/keystore)
Complex animations Reanimated worklets (stay in JS)
API calls JS (fetch/axios) — no native needed
Data transformation JS — unless processing >100MB

💻 Code Example

codeTap to expand ⛶
1// === TURBOMODULE: Full Example ===
2
3// 1. TypeScript Spec (shared between platforms)
4// specs/NativeSecureStorage.ts
5import type { TurboModule } from 'react-native';
6import { TurboModuleRegistry } from 'react-native';
7
8export interface Spec extends TurboModule {
9 // Synchronous — uses iOS Keychain / Android Keystore
10 setItem(key: string, value: string): boolean;
11 getItem(key: string): string | null;
12 removeItem(key: string): boolean;
13 getAllKeys(): string[];
14
15 // Async for potentially slow operations
16 clear(): Promise<void>;
17}
18
19export default TurboModuleRegistry.getEnforcing<Spec>('SecureStorage');
20
21// 2. Android Implementation (Kotlin)
22/*
23package com.myapp.modules
24
25import com.facebook.react.bridge.*
26import com.facebook.react.module.annotations.ReactModule
27import android.security.keystore.KeyGenParameterSpec
28import java.security.KeyStore
29
30@ReactModule(name = SecureStorageModule.NAME)
31class SecureStorageModule(reactContext: ReactApplicationContext)
32 : NativeSecureStorageSpec(reactContext) {
33
34 companion object {
35 const val NAME = "SecureStorage"
36 }
37
38 private val prefs by lazy {
39 // EncryptedSharedPreferences for secure storage
40 EncryptedSharedPreferences.create(
41 "secure_prefs",
42 MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
43 reactApplicationContext,
44 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
45 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
46 )
47 }
48
49 override fun getName() = NAME
50
51 override fun setItem(key: String, value: String): Boolean {
52 return try {
53 prefs.edit().putString(key, value).commit()
54 } catch (e: Exception) {
55 false
56 }
57 }
58
59 override fun getItem(key: String): String? {
60 return prefs.getString(key, null)
61 }
62
63 override fun removeItem(key: String): Boolean {
64 return prefs.edit().remove(key).commit()
65 }
66
67 override fun getAllKeys(): WritableArray {
68 val keys = Arguments.createArray()
69 prefs.all.keys.forEach { keys.pushString(it) }
70 return keys
71 }
72
73 override fun clear(promise: Promise) {
74 try {
75 prefs.edit().clear().commit()
76 promise.resolve(null)
77 } catch (e: Exception) {
78 promise.reject("CLEAR_ERROR", e.message)
79 }
80 }
81}
82*/
83
84// 3. iOS Implementation (Swift)
85/*
86@objc(SecureStorage)
87class SecureStorage: NSObject, NativeSecureStorageSpec {
88
89 @objc func setItem(_ key: String, value: String) -> Bool {
90 let data = value.data(using: .utf8)!
91 let query: [String: Any] = [
92 kSecClass as String: kSecClassGenericPassword,
93 kSecAttrAccount as String: key,
94 kSecValueData as String: data,
95 ]
96 SecItemDelete(query as CFDictionary) // Remove existing
97 let status = SecItemAdd(query as CFDictionary, nil)
98 return status == errSecSuccess
99 }
100
101 @objc func getItem(_ key: String) -> String? {
102 let query: [String: Any] = [
103 kSecClass as String: kSecClassGenericPassword,
104 kSecAttrAccount as String: key,
105 kSecReturnData as String: true,
106 ]
107 var result: AnyObject?
108 let status = SecItemCopyMatching(query as CFDictionary, &result)
109 guard status == errSecSuccess, let data = result as? Data else { return nil }
110 return String(data: data, encoding: .utf8)
111 }
112}
113*/
114
115// 4. Usage in JS — same API regardless of platform
116import SecureStorage from './specs/NativeSecureStorage';
117
118// Synchronous access (JSI-powered)
119const token = SecureStorage.getItem('auth_token');
120if (token) {
121 api.setAuthHeader(token);
122}
123
124// Store new token
125const success = SecureStorage.setItem('auth_token', newToken);
126if (!success) {
127 ErrorReporter.report(new Error('Failed to store auth token'));
128}

🏋️ Practice Exercise

Native Module Exercises:

  1. Write a TurboModule spec for a device information module (battery level, device model, OS version)
  2. Implement a native module that accesses the platform's biometric authentication (Face ID / Fingerprint)
  3. Create a native module that wraps a third-party SDK (e.g., a payment provider)
  4. Build a native module that performs heavy image processing (resize, crop) off the JS thread
  5. Implement a platform-specific module with different behavior on iOS vs Android using the same JS API
  6. Write comprehensive tests for a native module — mock the native side and test the JS interface

⚠️ Common Mistakes

  • Not handling errors in native modules — native exceptions that propagate to JS cause cryptic crashes

  • Running heavy operations on the main thread in native code — blocks UI just like blocking the JS thread

  • Forgetting to handle the case where the native module is not available — always check for null from TurboModuleRegistry

  • Not properly cleaning up native resources — event listeners, timers, and callbacks in native code must be deallocated

  • Using the old bridge-based NativeModules API in new projects — always use TurboModules with codegen for new modules

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Writing Native Modules (Android & iOS). Login to unlock this feature.