How to Use React Native Exclusively as a Data Layer

Ok, so I know this sounds counter intuitive. Why would you want to use a UI framework and not use the very thing it’s built to do best? There are definitely good reasons to not use this approach. However, in some situations, using React Native exclusively as a data layer can make sense and provide some benefits, so let’s entertain the idea and take a look at how we’d go about doing this. Spoiler if you’re lazy, we’re going to be using NativeModules
.
🍿 Why did I even want to do this?
In one of the projects I was working on last year, we had made the commitment to build a fully native UI for our mobile app to be able to better deliver on that, native-buttery UX customers have come to expect, with the limited amount of time we had, but at some point we realized that we needed to leverage an NPM package in our data layer. This posed a problem as we needed a quick solution to communicate between our web and native code. That’s when we stumbled upon using React Native exclusively as a data layer.
React Native is a really popular framework to build mobile user interfaces (for the most part, believe it or not, React Native is now also supported in both; Mac and Windows 🎉) that allows developers to use JavaScript to code the app’s logic while still utilizing the native capabilities of iOS and Android. This means that pretty much all the code that rules the UI at a high-level, is written in Javascript (you know, so it can be shared across platforms). However, in some cases like ours, you may only be interested in using React Native as a data layer and ignoring its UI capabilities in favor of maintaining full control of the UI hierarchy on the native side. Well this is actually possible by using the bridge functionality provided by React Native to pass data between the JavaScript and native sides of the app, but it’s not really advertised with the wording I’m using here.
📲 How to set up the communication
You’ll need to start by setting up a new React Native project by following the instructions on the React Native website. I won’t really won’t go into too much detail with this. We’ll then create a new JavaScript module that will act as the data layer. This module is what will export functions that can be called from the native side of the app to request data.
On the web side you need to use React Native’s NativeModules
to create an interface for your module and then import it in your component. On the native side, using Swift or Objective-C you can access the exported functions by using the same NativeModules
module to call the JavaScript methods.
The Javascript side
Here’s a simple example of a NativeModules
implementation in TypeScript that focuses on transferring example data about Batman villains:
// villainData.ts
import { NativeEventEmitter, NativeModules } from 'react-native';
const { BatmanBridge } = NativeModules;
const bridgeEmitter = new NativeEventEmitter(BatmanBridge);
let villains: string[] = ["Joker", "Riddler", "Penguin"];
function handleGetVillains() {
// You can call any NPM package compatible with React Native here :)
BatmanBridge.emitVillains(villains);
}
function handleAddVillain(name: string) {
villains.push(name);
BatmanBridge.emitVillains(villains);
}
bridgeEmitter.addListener('getVillains', handleGetVillains);
bridgeEmitter.addListener('addVillain', handleAddVillain);
The villainData.ts
file exports two functions, handleGetVillains
and handleAddVillain
which interact with a NativeModule called BatmanBridge
. handleGetVillains
is listening to the 'getVillains' event, and when it's triggered it emit all the villains back to the native side by calling BatmanBridge.emitVillains(villains)
and handleAddVillain
is listening to the 'addVillain' event, and when it's triggered it add the new villain passed as an argument to the villains
array, and then emit the updated list of villains back to the native side. Speaking of which, let’s take a look at how to set it up.
The native side
Let’s take a look at the corresponding Objective-C file that’ll talk to our new Typescript API.
// BatmanBridge.m
#import "BatmanBridge.h"
#import <React/RCTEventEmitter.h>
typedef void (^VillainRequestCompletion)(NSArray<NSString *> *villians, NSError *_Nullable error);
@implementation BatmanBridge
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(emitVillains:(NSArray<NSString *> *)villains)
{
NSLog(@"%@", villains);
}
-(NSArray<NSString *> *)getVillains
{
[self sendEventWithName:@"getVillains" body:nil];
}
-(NSArray<NSString *> *)addVillain:(NSString *)name)
{
[self sendEventWithName:@"addVillain" body:name];
}
- (NSArray<NSString *> *)supportedEvents
{
return @[@"getVillains", @"addVillain"];
}
@end
To tie it all together end-to-end you might need to do the rest of the proper React Native configuration (like setting up your RCTBridge
in our case. And don’t forget to return your BatmanBridge
instance to RN as part of the extraModulesForBridge
method call!) but for those parts of the process, there are a lot more resources out there so I want to keep this article scoped to this use case.
💡 Inevitable pros and cons
Using this approach, you should be able to use the power of the NPM ecosystem while keeping their UI completely native and optimized for each platform. However, as with any solution, there are pros and cons to consider. Let’s start with the pros:
- The ability to use npm packages in the data layer
- Separating the concerns of the data layer from the UI layer.
- Keeping the UI completely native and optimized.
Cons:
- React Native could be an overhead for the only purpose of a data layer.
- The complexity of setting up the communication between the JavaScript and native code.
- Not all npm packages may work seamlessly with React Native
React Native can be used exclusively as a data layer to communicate between Web and Native sides while ignoring its UI capabilities, but that doesn’t mean that you should. Make sure you really need to follow this approach. But if you really do need this like me at some point, here’s the article I wish I had :)