Bridging the Gap: Seamless Communication Between React Native and React Web

Subhashini Sahu
The Good Food Economy
9 min readSep 20, 2024

--

AI Generated Image

Introduction

In today’s fast-paced development environment, building versatile and responsive applications often requires leveraging the strengths of multiple frameworks. React Native and React Web are powerful tools in our toolkit, each offering unique benefits. However, when it comes to integrating the functionalities of these two platforms, things can get tricky. This is a story of how we tackled such a challenge head-on.

The Challenge

During our transition of core logic and UI components from a React Native app to a React Progressive Web App (PWA), we encountered a significant challenge. The React PWA needed to repeatedly access various React Native functionalities, such as camera access and location data, throughout the user experience. This requirement drove us to develop a robust bridge between React Native and React Web, enabling seamless bidirectional communication and functionality exchange.

Overview

Our project focuses on creating a strong communication channel between React Native and React Web. This allows us to access device permissions from React Native (like camera access) and location data from React Web. By doing this, we can effectively use the responses to perform necessary actions or callbacks.

Component Overview

React Web

  • The React web application runs in a browser environment, using HTML, CSS, and JavaScript to render user interfaces.
  • Components are built using React’s component-based architecture.

React Native WebView

  • The WebView component from React Native is key to our integration. It acts as an embedded web browser within the mobile app, displaying web content and enabling two-way communication.
import { WebView } from 'react-native-webview';

// WebView component integration
<WebView
source={{ uri: 'https://example.com' }}
onMessage={(event) => handleMessage(event.nativeEvent.data)}
/>

Commonly Used WebView Props:

  • source: Specifies the URL of the web content to be loaded.
  • onMessage: Handles messages received from the web content.
  • javaScriptEnabled: Enables JavaScript execution.
  • injectedJavaScript: Injects JavaScript code into the WebView.
  • onLoad, onLoadStart, onLoadEnd, onError: Various event handlers are used to manage WebView's lifecycle.

Bridge Communication

  • The bridge allows communication between the React Native app and the WebView component.
  • It enables data exchange, function calls, and event handling between the two environments.
  • The bridge consists of mechanisms for sending messages from React Native to WebView and vice versa.

Sequence of Communication

1. React Native to Web Communication

React Native sends messages to the WebView by injecting JavaScript code. This code can call functions or change the DOM in the WebView environment.

// React Native injects JavaScript into WebView
webViewRef.current.injectJavaScript('myFunction()');

In the web environment, define global functions or variables on the window object to listen for messages. React Native can call these global functions or access variables to communicate with the web content.

// This function is called after Native calls
window.myFunction = function(data) {
// Process data received from React Native
};

2. Web to React Native Communication

  • Web content inside the WebView sends messages to the React Native app using the window.postMessage() method.
  • The message sent from the web typically contains string or JSON data
  • React Native listens for these messages using the onMessage event handler on the WebView component.
  • When a message is received, React Native extracts the data from the message and takes appropriate actions based on the received data.
// Web sends a message to React Native
window.postMessage("Hello from Web!");
// React Native listens for messages from WebView
<WebView
onMessage={(event) => handleMessage(event.nativeEvent.data)}
/>

Using RPC to Set Up the Bridge

What is RPC (Remote Procedure Call)?

RPC is a way for a program to run a function in another address space (usually on another machine) as if it were a local function call. It helps different processes or systems talk to each other. Using the RPC bridge, we’ve created a system to receive unique responses for unique requests. By giving each request a unique ID, we ensure that each response is handled correctly.

How RPC Enhances the Bridge

  1. Smooth Communication: RPC allows smooth communication between React Native and WebView environments.
  2. Function Calls: Enables calling functions across the bridge.
  3. Data Exchange: Facilitates sharing information and performing actions based on received data.
  4. Asynchronous Operations: Supports operations that don’t block the client while waiting for a response.
  5. Abstraction: Hides the complexities of communication, simplifying the implementation.

Architecture

The bridge is implemented on both the web and native sides. Each side can send requests to the other, and each request is handled asynchronously. Responses are linked to their respective requests using unique IDs generated by the uuid library.

Example Scenarios:

#1. Taking Permission, Capturing Images in React Native, displaying on Web

Flow Diagram for Taking Permission, Capturing Image in React Native and Displaying in Web

Explanation:-

  • The web requests permission from the native app to access the device’s camera.
  • Upon permission is granted, the web opens the device’s camera interface to capture the image.
  • The user clicks a photo using the camera interface.
  • After clicking the photo, the image data is sent back to the native app for further processing or display.

#Code Example to create a bridge using RPC for the above example:

Below I have code examples of how to create an RPC Bridge and use it on both Native and Web:

  • Callbacks: When a request is sent (sendRequestToWeb or sendRequestToNative), a callback function is stored in the callbacks object with the unique callback_id as the key. When a response is received, the callback_id in the response is used to find and execute the corresponding callback function, ensuring that the correct response is delivered to the correct request.

1. React Web Project

  • Logic to trigger RPC request:
//here we are creating an rpcRequest and sending "Camera" as a method to be triggered on the App side and the callback "StoreImageInWeb" to be triggered on the Web side once permission is given and image is captured
const checkCameraPermission = () => {
const rpcRequest = createRpcRequest("Camera", { text: "Ope RN Camera from Web" }, "StoreImageInWeb")
sendMessageToApp(rpcRequest, window)
}
  • Create rpcBridge.js and bridgeFunctions.js for bridge setup:
// bridgeFunctions.js

export function StoreImageInWeb(params) { //callback function that will be triggered in web
if (params) {
alert(`Check Callback ${params}`) //receive the image and store it to display in the web
sessionStorage.setItem('imageUrl', params)
}
return true
}

2. React Native Android Project

App Side (rpcBridge.js)

  1. Singleton Bridge Instance: The createBridge function is a singleton, ensuring that only one instance of the bridge is created.

2. Creating Request and Response Bodies: _createRpcRequestBody and _createRpcResponseBody functions generate unique IDs for each request and response using uuid.v4(), ensuring that each call can be uniquely identified and handled.

3. Sending Messages: _sendRpcMessage sends a stringified JSON message to the web application through the WebView using injectJavaScript.

4. Handling Requests and Responses:

  • sendRequestToWeb is used to send a request to the web application. It creates a request body, stores the callback using a unique ID, and sends the request.
  • _sendResponseToWeb sends a response back to the web application.
  • listenMessagesFromWeb listens for messages from the web application. It distinguishes between responses to earlier requests and new requests based on the presence of callback_id.

The rpcBridge happens to be the most important file with the following components:

App Side (rpcBridge.js)

  1. Singleton Bridge Instance: The createBridge function is a singleton, ensuring that only one instance of the bridge is created.
  2. Creating Request and Response Bodies:_createRpcRequestBody and _createRpcResponseBody functions generate unique IDs for each request and response using uuid.v4(), ensuring that each call can be uniquely identified and handled.
  3. Sending Messages:_sendRpcMessage sends a stringified JSON message to the web application through the WebView using injectJavaScript.
  4. Handling Requests and Responses:sendRequestToWeb is used to send a request to the web application. It creates a request body, stores the callback using a unique ID, and sends the request.
  • _sendResponseToWeb sends a response back to the web application.
  • listenMessagesFromWeb listens for messages from the web application. It distinguishes between responses to earlier requests and new requests based on the presence of callback_id.

5. Bridge Functions:

  • These are the actual functions that will be called by the web application. They are defined in a separate file (bridgeUtilities) and handle various tasks such as setting tokens, opening URLs, dialling phone numbers, etc.

2. React Native Android Project

import React, { useRef } from 'react';
import { WebView } from 'react-native-webview';
import { listenMessagesFromWeb, sendMessageToWeb, createRpcRequest } from './rpcBridgeV2';

export default function LocationWebView({ route }) {
const webViewRef = useRef(null)
const testing = () => {
const rpcRequest = createRpcRequest("LocationPermissionAlert", { text: "Get Location Permission" }, "PrintLocation")
sendMessageToWeb(rpcRequest, webViewRef)
}
const onMessage = (event) => {
listenMessagesFromWeb(event, webViewRef);
}
return (
<WebView
onMessage={event => {
onMessage(event.nativeEvent.data)
}}
source={{ uri: 'http://localhost:3000' }} // Replace with your PWA URL
ref={webViewRef}
onLoad={testing}
/>
)
}
  • Create rpcBridge.js and bridgeFunctions.js for bridge setup:
//rpcBridge.js
import * as bridgeFunctions from "./bridgeFunctions";

// Function to create a JSON-RPC request object
const createRpcRequest = (method, params, callback) => {
return { id: "1234", method: method, params: params, callback: callback }
}
// Function to send a JSON-RPC request with a callback method to the Web
const sendRpcRequestForCallback = (request, response, webViewRef) => {
// Checking if a callback method is specified in the request
if (request?.callback) {
// Creating a new request object for the callback
const callbackRequest = {
id: request.id,
method: request.callback,
params: response,
callback: null,
}
sendRpcRequest(callbackRequest, webViewRef)
}
else {
console.log("No CallBack Found(RN)")
}
}
// Function to send a JSON-RPC request to the React Web
const sendRpcRequest = (request, webViewRef) => {
const requestStrigified = JSON.stringify(request)
webViewRef?.current?.injectJavaScript(`window.listenMessagesFromApp('${requestStrigified}')`)
}
// Function to send a message to the web
const sendMessageToWeb = (request, webViewRef) => {
sendRpcRequest(request, webViewRef)
}
// Function to listen for messages from the web
async function listenMessagesFromWeb(request, webViewRef) {
const parseRequest = JSON.parse(request)
let { id, method, params, callback } = parseRequest;
const response = await bridgeFunctions[method](params) //calls methods from bridgeFunctions file and executes them on the basis of method name and also send params to the specific method
sendRpcRequestForCallback(parseRequest, response, webViewRef)
};
export { sendMessageToWeb, sendRpcRequestForCallback, sendRpcRequest, listenMessagesFromWeb, createRpcRequest };
// bridgeFunctions.js
import { PermissionsAndroid, Platform } from "react-native";
import { launchCamera } from 'react-native-image-picker';
import RNFetchBlob from 'rn-fetch-blob';

export const Camera = async () => {
try {
const hasPermission = await checkCameraPermission(); //function to check camera permission in it
if (!hasPermission) {
console.log('Camera permission denied');
return null;
}
const options = {
mediaType: 'photo',
quality: 0.5,
maxWidth: 800,
maxHeight: 600,
};
//get response and return response to web
const response = await new Promise((resolve, reject) => {
launchCamera(options, (response) => {
resolve(response);
});
});
} catch (error) {
console.error('Error:', error);
return null;
}
};

Web Side (rpcBridge.js)

1. Singleton Bridge Instance: Similar to the app side, the web side also uses a singleton pattern to create the bridge.

2. Creating Request and Response Bodies: Similar functions for creating request and response bodies, ensuring unique IDs for each message.

3. Sending Messages: _sendRpcMessage uses postMessage to send messages to the native app.

4. Handling Requests and Responses:

  • sendRequestToNative sends a request to the native app and stores the callback.
  • _sendResponseToWeb sends a response back to the native app.
  • listenMessagesFromNative listens for messages from the native app and processes them similarly to the app side.

Difference Between RPC Bridge and Normal WebView Functions

While normal WebView functions offer basic communication between the React Native app and web content, the RPC bridge provides several enhancements:

Normal WebView Functions

  • Basic Messaging: Uses window.postMessage for web-to-native communication and injectJavaScript for native-to-web communication.
  • Simple Data Exchange: Primarily used for sending and receiving strings or JSON data.
  • Limited Control: Functions are limited to basic messaging without advanced features like function calls or asynchronous handling.

RPC Bridge

  • Advanced Communication: Facilitates more complex interactions, including calling functions and handling responses asynchronously.
  • Function Calls: Allows calling functions across the bridge, not just sending messages.
  • Data Exchange: Supports structured data exchange with better error handling and response management.
  • Asynchronous Operations: Enables asynchronous communication, enhancing performance and user experience.
  • Abstraction: Hides the complexities of communication, simplifying the implementation.

By leveraging the RPC bridge, we achieve a more robust and flexible communication channel, allowing us to build more interactive and responsive applications.

Reference:-

Conclusion

Establishing a communication bridge between React Native and React Web using React Native WebView and RPC opens up a wide array of possibilities for building integrated and feature-rich applications. By leveraging these technologies, developers can achieve seamless functionality exchange and enhance user experience across different platforms.

--

--