Creating Your First Native Module Library for React Native: A Step-by-Step Guide

Ahmed Kamal
8 min readJun 24, 2024

--

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:

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:

  1. Consistency Across Platforms: JSON keeps data formats consistent when moving between JavaScript and native code (Android and iOS).
  2. Easy to Use: JSON is simple to parse and generate. It’s well-supported with tools like GSON for Android and JSONSerialization for iOS.
  3. Handles Complex Data: JSON can easily manage and serialize complex nested structures, keeping all details intact.
  4. 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!

--

--

Ahmed Kamal
Ahmed Kamal

Written by Ahmed Kamal

Software Engineer @noon | Ex-Fawry | ITI-Certified

No responses yet