Creating Your First Native Module Library for React Native: A Step-by-Step Guide
Hey there! dealing with bridges can be intimidating at first, that’s why I wanted to simplify it a little bit for you because once you get the gist of it, it will be an easy routine for you.
I will go over building a React Native Library with native modules for both Android and iOS. This is essentially a bridge that sits between your native and JavaScript/TypeScript code. Today, I want to share some of the tricks that I had to learn the hard way and make your life easier.
Just so we’re on the same page, we can call this native module a library, an SDK, a package, or a plugin depending on what you’re used to saying. Of course, there are differences between those names, but for simplicity, let’s think of them as basically the same.
Setting Up the Project
First, let’s set up our project. Open your terminal and run:
npx create-react-native-library@latest bridge-guide
Making Choices
You’ll be prompted to make some selections. Here’s what you should choose:
- Name of the npm package:
bridge-guide
- Description:
Tutorial on creating native modules in React Native
- Package author:
Ahmed Kamal
- Email address:
AKreview22@gmail.com
- URL for the author:
https://github.com/AKreview22
- URL for the repository:
https://github.com/AKreview22/bridge-guide
- Type of library:
Native module
- Languages:
Kotlin & Swift
Most of these are self-explanatory, but let’s dive into one key choice:
Understanding Native Modules vs. Native Views
- Type of library:
Native module
- This ensures we’re using a template with both Android and iOS native code.
If you wonder why we chose “native module,” it’s because we need functionality handled by native code but called from JavaScript.
If we had chosen the Native view
, then we would have gotten a template for opening a native UI view from React Native, and this could be useful when implementing UI in native code. But since we're doing the UI with React Native for more consistency and less redundancy, we're going with Native modules
.
Simply put, choose the Native view
only if you already have the UI implemented in your iOS/Android code or plan to implement it there.
You can also choose to use the JavaScript library instead if you will not write any native code.
Using IDEs for Development
Instead of just writing all your code using one IDE like Visual Studio Code, use Visual Studio Code for React Native, Android Studio for Android, and Xcode for iOS. These IDEs provide autocomplete, import suggestions, and built-in tools for building and debugging your project.
Project Structure
Now, let’s get to know the project better. The example
directory is your host app. Think of it as your sandbox to test the library you are developing, your usual React Native app, and an "Example"—duh—of how your client should use your module.
Everything outside the example
directory is about the module you're developing.
Installing Dependencies
To get started, navigate to your project directory and run:
yarn
yarn example ios
yarn example android
This will open a terminal window for interacting with your host app.
Checking Out the Code
Let’s take a look at the code. Our starting point includes a simple multiplication function using a promise.
src/index.tsx
export function multiply(a: number, b: number): Promise<number> {
return BridgeGuide.multiply(a, b);
}
This function invokes the native module for multiplication, defining the function we’ll use in our host app (in TypeScript or JavaScript) and forwarding the call to its native counterpart for execution.
Android Implementation
Android/src/main/java/com/bridgeguide/BridgeGuideModule.kt
@ReactMethod
fun multiply(a: Double, b: Double, promise: Promise) {
promise.resolve(a * b)
}
We use @ReactMethod
to expose this method to React Native. promise.resolve
handles success, and promise.reject
handles errors.
iOS Implementation
For iOS, you need to rely on both the .mm and .swift files to properly expose functionality to React Native (TypeScript/JavaScript).
BridgeGuideModule.mm
RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
BridgeGuideModule.swift
@objc(multiply:withB:withResolver:withRejecter:)
func multiply(a: Float, b: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
resolve(a * b)
}
similar to android resolve
handles success and reject
handles the ios.
Using the Module in the Example App
example/src/App.tsx
import * as React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { multiply } from 'bridge-guide';
export default function App() {
const [result, setResult] = React.useState<number | undefined>();
React.useEffect(() => {
multiply(3, 7).then(setResult);
}, []);
return (
<View style={styles.container}>
<Text>Result: {result}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Adding More Complexity: Book Management
Real-life projects can be a bit more complex, but don’t worry, it doesn’t get much harder. I’ll show you how to implement it yourself with an example.
Let’s create a function that takes a book object, updates its availability, adds a timestamp for the last borrowed date, and returns the updated book as a string.
src/index.tsx
export function processBook(book: any): Promise<string> {
return BridgeGuide.processBook(book);
}
Android Implementation
Android/src/main/java/com/bridgeguide/BridgeGuideModule.kt
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactMethod
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ReactMethod
fun processBook(book: ReadableMap, promise: Promise) {
try {
val modifiedBook = book.toHashMap().apply {
this["isAvailable"] = false
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
this["lastBorrowed"] = sdf.format(Date())
}
promise.resolve("Modified Book: $modifiedBook")
} catch (e: Exception) {
promise.reject("BOOK_PROCESSING_ERROR", "Failed to process book", e)
}
}
iOS Implementation
BridgeGuideModule.mm
RCT_EXTERN_METHOD(processBook:(NSDictionary *)book
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
BridgeGuideModule.swift
@objc(processBook:withResolver:withRejecter:)
func processBook(book: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
var modifiedBook = book
modifiedBook["isAvailable"] = false
let dateFormatter = ISO8601DateFormatter()
modifiedBook["lastBorrowed"] = dateFormatter.string(from: Date())
resolve("Modified Book: \\(modifiedBook)")
}
Using processBook
in the Example App
example/src/App.tsx
import * as React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { processBook } from 'bridge-guide';
export default function App() {
const [result, setResult] = React.useState<string | undefined>();
React.useEffect(() => {
const book = {
title: "React Native for Beginners",
author: "John Doe",
isAvailable: true,
lastBorrowed: null,
details: {
publisher: "Tech Books Publishing",
year: 2021
}
};
processBook(book).then(setResult);
}, []);
return (
<View style={styles.container}>
<Text>Result: {result}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
A Better Way to Implement It
As you can see, implementing even the simplest feature would take a noticeable amount of time when dealing with large objects.
Of course, it was easier here because we just have a simple object, but think of a more complicated one like a book object with nested details about the publisher. It would take you a huge amount of time just to extract the needed data from this.
So, deal with it as if your “Swift and Kotlin” code are your backend. They provide values and those values should be JSON as we deal with any server.
We should also send data as JSON to it.
Benefits of Using JSON
Using JSON in your React Native projects, especially with complex objects, offers several key benefits:
- Consistency Across Platforms: JSON keeps data formats consistent when moving between JavaScript and native code (Android and iOS).
- Easy to Use: JSON is simple to parse and generate. It’s well-supported with tools like GSON for Android and JSONSerialization for iOS.
- Handles Complex Data: JSON can easily manage and serialize complex nested structures, keeping all details intact.
- Great Compatibility: JSON is widely used in web APIs, databases, and config files, making data exchange smooth and hassle-free with other services.
Improved Implementation with JSON.
src/index.tsx
import { NativeModules } from 'react-native';
const { BridgeGuide } = NativeModules;
export function processBookJson(book: any): Promise<string> {
return BridgeGuide.processBookJson(JSON.stringify(book));
}
Android Implementation
Android/src/main/java/com/bridgeguide/BridgeGuideModule.kt
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactMethod
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ReactMethod
fun processBookJson(jsonString: String, promise: Promise) {
try {
val mapType = object : TypeToken<Map<String, Any>>() {}.type
val bookMap: Map<String, Any> = Gson().fromJson(jsonString, mapType)
val modifiedBook = bookMap.toMutableMap()
// Update availability status
modifiedBook["isAvailable"] = false
// Add last borrowed date
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
modifiedBook["lastBorrowed"] = sdf.format(Date())
// Convert back to JSON
promise.resolve(Gson().toJson(modifiedBook))
} catch (e: Exception) {
promise.reject("JSON_ERROR", "Failed to process JSON", e)
}
}
iOS Implementation
BridgeGuideModule.mm
RCT_EXTERN_METHOD(processBookJson:(NSString *)jsonString
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
BridgeGuideModule.swift
@objc(processBookJson:withResolver:withRejecter:)
func processBookJson(jsonString: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
do {
if let data = jsonString.data(using: .utf8),
var book = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// Update availability status
book["isAvailable"] = false
// Add last borrowed date
let dateFormatter = ISO8601DateFormatter()
book["lastBorrowed"] = dateFormatter.string(from: Date())
let modifiedData = try JSONSerialization.data(withJSONObject: book, options: [])
let modifiedJsonString = String(data: modifiedData, encoding: .utf8)
resolve(modifiedJsonString)
}
} catch {
reject("JSON_ERROR", "Failed to process JSON", error)
}
}
Using processBookJson
in the Example App
example/src/App.tsx
import * as React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { processBookJson } from 'bridge-guide';
export default function App() {
const [result, setResult] = React.useState<string | undefined>();
React.useEffect(() => {
const book = {
title: "React Native for Beginners",
author: "John Doe",
isAvailable: true,
lastBorrowed: null,
details: {
publisher: "Tech Books Publishing",
year: 2021,
address: {
street: "Main St",
buildingNumber: 123,
apartmentNumber: 45
}
}
};
processBookJson(book).then(setResult);
}, []);
return (
<View style={styles.container}>
<Text>Result: {result}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Third-Party libraries
Usually in your native code you will need to use a “library” , so here’s how you can do it on both platforms :
Integrating with Pods
To integrate with pods in iOS, you need to specify in your podspec
file which pods you want to install. This basically tells your host app that this library needs a certain pod to work.
podspec
File
Pod::Spec.new do |s|
s.name = "bridge-guide"
s.version = "0.0.1"
s.summary = "A React Native library example"
s.description = <<-DESC
A React Native library example showing how to use native modules in both Android and iOS.
DESC
s.homepage = "<https://github.com/AKreview22/bridge-guide>"
s.license = { :type => "MIT", :file => "LICENSE" }
s.author = { "Ahmed Kamal" => "AKreview22@gmail.com" }
s.source = { :git => "<https://github.com/AKreview22/bridge-guide.git>", :tag => "#{s.version}" }
s.platform = :ios, "10.0"
s.source_files = "ios/**/*.{h,m,swift}"
s.dependency 'React'
s.dependency 'GoogleMaps' # Example dependency
end
Integrating with Android
For Android, you might need to specify dependencies in your build.gradle
file.
build.gradle
File
dependencies {
implementation project(':bridge-guide')
implementation 'com.google.code.gson:gson:2.8.6' // Example dependency
}
Publishing to npm
Let’s quickly go over how to publish your package to npm. It’s simple:
npm login
npm run build
npm publish
That’s it! We created a React Native library, implemented functions in both Android and iOS, tested them, and showed how to publish the package.
I hope you found this tutorial helpful. Stay tuned for more detailed guides!