Building a gRPC Client in React Native on iOS

Adonis Gaitatzis
12 min readOct 27, 2021

In this article we will learn how to build a simple gRPC client in React Native by bridging a native module on iOS / Swift 5.

Overview

At the time of writing, gRPC isn’t supported by React Native. iOS supports it though.

React Native was designed to talk to Objective-C in iOS using native modules, but not to Swift. Since Swift can connect classes and methods to Objective-C bindings, it is possible to get React Native to act as a gRPC client by binding React Native to an Objective-C bridge, which talks to your Swift code.

Note that the grpc-web library is not compatible with React Native as of 0.66.

So, to get this working you will need to:

  1. Install the toolchain
  2. Create a React Native project
  3. Install iOS gRPC dependencies
  4. Compile Protobuffer files
  5. Program gRPC client in Swift
  6. Expose gRPC client methods as a native module
  7. Build React Native native module class for gRPC client
  8. Integrate with UI
  9. Build and test

The gRPC server we will be using is authService from another tutorial that shows how to build a gRPC Server in NodeJS. Our React Native client will be able to log in, retrieve an authentication token, and log out.

What is gRPC

gRPC is a way for clients and servers to communicate with each other over the Internet. It is intended as an upgrade from REST APIs, with some great features:

  • It transmits faster by using a binary format instead of JSON.
  • It uses a language-agnostic data format, which can be defined and passed. around to clients without the need for per-language developer documentation.
  • It translates its common data format into the native data formats of the server and client.
  • It supports versioning, so future versions of server code don’t require a total rewrite of the client.
  • It defines how to find the endpoints, so URL naming is no longer an issue.
  • It works over HTTP/3, which enables bidirectional streaming with less network latency.

The down sides (at the time of writing) are:

  • It requires custom compiler code for each language to perform all the conversions and binary format conversions.
  • The mechanics of operation are totally obscure to developers. It is not human-readable the way JSON and REST are.
  • The toolchains are new and not very well supported. Sometimes getting a simple call and response to work can take hours or days to get working.
  • It isn’t yet well supported or well documented on all platforms.
  • HTTP/3 is poorly supported by browsers and app software, especially over at Apple.

In this tutorial, I will attempt to walk you through the process I went through to get a simple gRPC client working in React Native on iOS.

How gRPC Works

Basically how gRPC works is that a developer creates a .proto file, which describes objects and methods which will be provided by a service. This file acts as a sort of developer documentation and a reference for a protobuffer compiler which converts the .proto definitions into the local language’s data types.

The protobuffer compiler creates a library that the developer can interact with, abstracting the messaging layer of the API.

This allows a developer to write API logic without having to manage API versions, HTTP endpoints, or JSON parsing.

Demo Server And Protobuffer Definition

For this tutorial we will create a simple Protobuffer definition called authService.protothat provides two methods, Login() and Logout().

syntax = "proto3";
package authService;
option objc_class_prefix = "RTG"; // important for iOS / Xcode// Define a service
// Define data types here
message AccountCredentials {
string username = 1;
string password = 2;
}message OauthCredentials {
string token = 3;
uint32 timeoutSeconds = 4;
}// Define the service containing methods here
service AuthServiceRoutes {
// Basic function call, makes request and returns value
rpc Login(AccountCredentials) returns (OauthCredentials) {}
rpc Logout(OauthCredentials) returns (OauthCredentials) {}
}

This authService.proto file will be available to both the client and server at the time of development, so that the protobuffer compiler can create RPC endpoints.

We can see how to build a RPC server in NodeJS from this protobuffer file, which we can use as the server portion of this tutorial.

Requirements

In order to build this project, we will need some development tools:

  • An Apple computer
  • Xcode and Command-Line-Tools, Apple’s IDE and compiler tools for building iOS projects (requires an Apple computer)
  • GoLang, a programming language made by Google
  • Homebrew, a Terminal-based MacOS software manager (requires Xcode)
  • Cocoapods, a package manager for Xcode project libraries and frameworks (requires Homebrew)
  • NodeJS, a command-line programming Javascript implementation
  • React Native, a universal front-end development framework for mobile applications (requires NodeJS)

This tutorial was written using React Native version 0.66.1, Swift version 5.4.2 on Xcode 12.5.1.

1. Install the Toolchain

Assuming you already have the required tools listed above set up properly, you can proceed with installing the toolchain, which consists of:

  • protobuf, a tool that compiles gRPC .proto definition files into native libraries.
  • grpc-swift, a plugin for protobuf that lets you compile .proto files into a Swift library

Install Protocol Buffer Compiler, which compiles .proto files into a native code library in one of several languages. Open a terminal and install protobuf using Homebrew:

$ brew install protobuf

This will install the protoc program, which compiles .proto files.

Next, install the protoc plugin that allows us to compile .proto files into Swift-compatible code.

$ mkdir ~/sandbox
$ cd ~/sandbox
$ git clone https://github.com/grpc/grpc-swift
$ cd grpc-swift
$ make plugins
$ cp .build/release/protoc-gen-swift .build/release/protoc-gen-grpc-swift /usr/local/bin

2. Create React Native Project

The first thing you must do is create a new React Native project. This will create a template React Native project which could be deployed on both iOS and Android, but in this tutorial we will only be dealing with the iOS side of things.

$ npx react-native init RnNativeGrpcClient
$ cd RnNativeGrpcClient

Now is a good time to compile and make sure there are no problems. Run this command to deploy onto your iOS Simulator:

$ npx react-native run-ios
# or
$ npm run ios
A React Native App

3. Install iOS gRPC Dependencies

The next step consists of installing the Cocoapod frameworks that allow you to use the gRPC APIs in your Swift code.

$ open . &  #open Finder
$ open ios/RnNativeGrpcClient.xcworkspace & # Open Xcode project

Open the Podfile and make these three changes:

  1. Update to target version “14”:
platform :ios, ‘14.0’

2. Remove use_flipper!() and replace with use_frameworks!

3. Add the following lines after use_frameworks! to tell Cocoapods to install the gRPC-Swift frameworks.

pod ‘gRPC-Swift’, ‘~> 1.5.0’ # Latest at the time of writing
pod ‘gRPC-Swift-Plugins’

With those changes, you can install the new frameworks from your Terminal and watch the magic happen:

$ npx pod-install

4. Compile Protobuffer file

Remember protobuf and grpc-swift? Using the protoc command, you can compile a .proto file into a Swift library.

Place the authService.proto in your Xcode project folder and run this command:

$ cd /path/to/xcode/project
$ protoc authService.proto \
--grpc-swift_opt=Client=true,Server=false \
--grpc-swift_out=ios/
$ protoc authService.proto \
--proto_path=. \
--swift_opt=Visibility=Public \
--swift_out=ios/

That generates two new files:

  • authService.grpc.swift, which contains the implementation of your generated service classes
  • authService.pb.swift, which contains the implementation of your generated message classes

These files describe the service and the related message classes derived from the .proto files.

For example,

  • AuthServiceRoutes in the Protobuffer file will be mapped to AuthService_AuthServiceRoutesClient in Swift.
  • AccountCredentials in the Protobuffer file will be mapped to AuthService_AccountCredentials in Swift
  • OauthCredentials in the Protobuffer file will be mapped to AuthService_OauthCredentials

Drag these two new files (authService.grpc.swift and authService.pb.swift) into the Xcode project under <xcode-project-name/xcode-project-name> and select “Copy Items if Needed” and “Create folder references” to make sure they are included in the compile later.

You’ll be dragging it into the same folder as the Info.plist.

Now is a good time to build the project to make sure there are no errors.

$ npx react-native run-ios

5. Expose gRPC Client Methods as Native Module

In Xcode, Create a new Swift File (Cmd+n). For this tutorial, we will name it AuthClient. Click “Next

Create New Swift File
Name the Swift File

Xcode will ask if you want to create an Objective-C bridging header. Since you are bridging your Swift code to React-Native through Objective-C, that’s exactly what we want. Click “Create Bridging Header

Create a Bridging Header

You’ll end up with two new files:

  • AuthClient.swift
  • <project-name>-Bridging-Header.h

To bridge Objective-C to React-Native, you’ll need to import the RCTBridgeModule.h

Add this to <project-name>-Bridging-Header.h:

#import “React/RCTBridgeModule.h”

Now let’s look at AuthClient.swift. This will serve as both the gRPC client and as the Objective-C bridge to React Native.

Normally in Swift, the methods would look like this:

class ClassName {
func methodName() {}
}

However, since we need to bridge through Objective-C, we need to add @objc decorators to our Swift code, like this:

@obj(ClassName) class ClassName: NSObject {
@obj func login() {}
}

Import Frameworks

Start by importing GRPC and NIO frameworks:

import Foundation
import GRPC
import NIO

Define Class

The next step is to define the AuthClient class and connect the AuthService_AuthServiceRoutesClient (the class that was auto-generated by the protoc command). We happen to know from the other tutorial that the server is running on localhost:50051

@objc(AuthClient) class AuthClient: NSObject {
var authServiceClient: AuthService_AuthServiceRoutesClient?
let port: Int = 50051
}

We don’t need this to run on the main thread, so we can set requiresMainQueueSetup() to return false :

@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
}

Let’s create a method that opens a channel to the gRPCC server. This method does not need to be exposed to React Native:

@objc(AuthClient) class AuthClient: NSObject {
// ...
func createGrpcServiceClient(
eventLoopGroup: EventLoopGroup
) throws -> Lnrpc_WalletUnlockerClient {
let secureGrpcChannel = try GRPCChannelPool.with(
target: .host("localhost", port: 51009),
transportSecurity: .plaintext,
eventLoopGroup: eventLoopGroup
)
return AuthService_AuthServiceRoutesClient(
channel: secureGrpcChannel
)
}
}

Don’t get too excited yet, you’ll be calling this method in your Swift code like this:

// create event loop
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
}
// close the eventLoop when the function exits
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
// retrieve the AuthService_AuthServiceRoutesClient
let authServiceRoutesClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
)

Implement gRPC Methods

The gRPC client should implement two methods: login() and logout():

Since talking to a remove server may take a noticeable amount of time, we don’t want to block the UI thread. Therefore we will be implementing these methods using Javascript Promises.

Since Promises aren’t supported natively in Swift, they are implemented as callbacks using the RCTPromiseResolveBlock and RCTPromiseRejectBlock objects.

These callback objects must be the last two parameters in a function, and all parameters must be unnamed.

So the process is this:

@obj func methodName(
_ param1: Object,
param2: Object,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
// Do stuff and resolve a dictionary
resolve(["key1": "value1", "key2": "value1"])
}

Then you can call this method in JavaScript:

try {
const grpcMessageResponse = nativeModuleName.methodName(
param1,
param2
)
} catch (error) {
console.log(error.message)
}

Back in Swift, You can throw an error using the reject callback:

let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "This is the error.message text", error)

To execute the method, you must grab it from the client object:

let call = serviceClientObject.methodName(param1 param2)
let responseObject = call.response.wait() // throws error on fail

Parameters are usually gRPC methods, which can be built like this:

let messageTypeName: ServiceName_MessageTypeName = .with {
$0.param1 = "value1"
$0.param2 = "value2"
}

Putting that all together, we can write the login() method:

@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc func login(
_ username: String,
password: String,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
)
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
let authServiceClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
)
// build the AccountCredentials object
let accountCredentials: AuthService_AccountCredentials = .with {
$0.username = username
$0.password = password
}
// grab the login() method from the gRPC client
let call = authServiceClient.login(accountCredentials)
do {
let oauthCredentials = try call.response.wait()
resolve([
"token": oauthCredentials.token,
"timeoutSeconds": oauthCredentials.timeoutSeconds
])
} catch {
let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "RPC method ‘login’ failed", error)
}
}
try? authServiceClient.channel.close().wait()
}

And the logout() method:

@objc(AuthClient) class AuthClient: NSObject {
// ...
@objc func logout(
_ oauthToken: String,
resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
let eventLoopGroup = PlatformSupport.makeEventLoopGroup(
loopCount: 1
)
defer {
try? eventLoopGroup.syncShutdownGracefully()
}
let authServiceClient = try self.createGrpcServiceClient(
eventLoopGroup: eventLoopGroup
)
// build the OauthCredentials object
let oauthCredentials: AuthService_OauthCredentials = .with {
$0.token = oauthToken
}
// grab the logout() method from the gRPC client
let call = authServiceClient.logout(oauthCredentials)
// execute the gRPC call and grab the result
do {
let logoutResult = try call.response.wait()
print("Logged out")
resolve([
"token": logoutResult.token,
"timeoutSeconds": logoutResult.timeoutSeconds]
)
} catch {
let error = NSError(domain: "", code: 200, userInfo: nil)
reject("0", "RPC method ‘logout’ failed", error)
}
}
try? authServiceClient.channel.close().wait()
}

Export Methods through the Bridge

Now that you have the gRPC client code and the Objective-C bridge, you’ll need to push those client methods through the bridge to expose them to the React Native native module system.

To do this, you must create a new Objective-C file (Cmd+n), and name it the same as your previous Swift file (in this case, AuthClient.m):

Create New Objective-C File

In order to export a class to React Native, you must include the RCTBridgeModule.h framework in this file.

The export syntax goes like this:

@interface RCT_EXTERN_MODULE(ClassName, NSObject)
RCT_EXTERN_METHOD(methodWithouParams)
RCT_EXTERN_METHOD(
asyncMethodWithoutParams: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
RCT_EXTERN_METHOD(
asyncMethodWitParams: (NSString *) param1
param2Name: (NSString *) param2
resolve: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
@end

We will be exporting a class AuthClient , which has three methods: init(), login() and logout(), so the AuthClient.m file looks like this:

#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(AuthClient, NSObject)
RCT_EXTERN_METHOD(init)
RCT_EXTERN_METHOD(
login: (NSString *)username
password:(NSString *)password
resolve: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
logout: (NSString *) oauthToken
resolve: (RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject
)
@end

Now is a great time to build in your Terminal again to make sure everything is still working:

$ npx react-native run-ios

6. Build React Native Native Module for gPRC Client

Now that you have the Swift gRPC client built and bridged through Objective-C, it’s time to build the corresponding Javascript/React Native library.

For this tutorial, we won’t create any components or views. We will just edit the App.js to keep things simple.

In your App.js , import the NativeModules module and then the AuthClient that was exposed through the bridge:

import { NativeModules } from "react-native"
const { AuthClient } = NativeModules

Now you can load the AuthClient in the app on first run:

import React, { useState, useEffect } from 'react'
import { NativeModules } from 'react-native'
const { AuthClient } = NativeModules
// ...const App: () => Node = () => {
const [isFirstRun, setIsFirstRun] = useState(true)
useEffect(() => {
if (isFirstRun) {
AuthClient.init()
setIsFirstRun(false)
}
}, [isFirstRun])
// ...
}

Let’s also implement the login() and logout() methods. We will store the authentication token so we can display it to the user:

// ...
const App: () => Node = () => {
const [username, setUsername] = useState('email@example.com')
const [password, setPassword] = useState('password')
const [oauthToken, setOauthToken] = useState('')
const login = async (username, password) => {
try {
const oauthData = await AuthClient.login(username, password)
setOauthToken(oauthData.token)
} catch (error) {
console.log(error)
console.log(error.message)
}
}
const logout = async (oauthToken) => {
await AuthClient.logout(oauthToken)
setOauthToken(null)
}
// ...
}

Now is a great time to build to make sure everything still works :-)

7. Integrate with UI

Now that you have the logic, you can throw up a simple UI to connect everything.

This shouldn’t be anything too new if you are reading this, so I won’t spend any time on the implementation. It’s subject to your implementation anyway.

import {
SafeAreaView,
View,
TextInput,
Text,
Button
} from 'react-native'
// ...const App: () => Node = () => {
// ...
return (
<SafeAreaView style={styles.backgroundStyle}>
<TextInput
type="email"
placeholder="Email"
autoCapitalize="none"
value={username}
onChangeText={setUsername}
style={styles.textInput}
/>
<TextInput
type="password"
placeholder="Password"
autoCapitalize="none"
value={password}
onChangeText={setPassword}
style={styles.textInput}
/>
<Button
title="Login"
onPress={() => { login(username, password) }}
/>
<Button
title="Logout"
onPress={() => { logout(oauthToken) }}
/>
<Text>Oauth Token: {oauthToken}</Text>
</SafeAreaView>
)
}

8. Build and Test

As always, build and test. Make sure you are running your gRPC server of course.

$ npx react-native run-ios

That’s it! You should have a working gRPC client in React Native on your iOS Simulator!

Further Reading

Build a gPRC Server in NodeJS

Building a gRPC Client in iOS / Swift

React Native Native Modules in iOS / Swift

React Native Native Modules Official Documentation.

TeaBreak has a a fabulous tutorial about how to return various types of data from Swift Methods in Swift in React Native — The Ultimate Guide Part 1: Modules.

--

--

Adonis Gaitatzis

Is a technology and branding nerd who can synthesize any business need into a technology solution. Fun projects have included brain imaging tech & mesh routers.