Network layer in React Native

This September started for me with a great experience giving a talk about network layer in React Native at React Native Europe conference.

Unfortunately, not everyone was able to attend the conference and/or listen to my talk, so for those who’s interested, I’d like to provide an article explaining the concept I shared.

Motivation

Nowadays, it is hard to imagine a mobile application without any kind of network communications. Currently, my phone contains more than 40 applications, but there is only one that doesn’t use any of them — calculator. For me it is a solid indicator that it is almost inevitable to avoid writing network-specific code in an average React Native app.

In this article we’ll cover all networking primitives that React Native expose to JavaScript, make a round trip request and discuss most common network issues you may encounter using the platform.

Meet our heroes!

React Native ships with three network APIs available by default: fetch, XMLHttpRequest and WebSocket. There is one thing about all of them that some developers may not know: neither of these APIs is a part of ECMAScript standard which means that JavaScript doesn’t have any standardised ways of transmitting packages over the wire.

fetch, XMLHttpRequest and WebSocket APIs

Therefore, if we want to perform network requests from JavaScript, we need to extend its runtime by a custom functionality implemented by the underlying host platform (which is React Native in our case).

There is a chance that you are not familiar with terms “runtime” and “host platform”: a runtime represents a complete JavaScript execution environment which means it contains all the APIs that are available for developer. Host platform is a platform that embeds JavaScript language and therefore can extend its runtime (by adding custom variables and functions to it).

If we oversimplify JavaScript runtime, it may look like this:

Oversimplified JavaScript runtime. JS API consists of a custom host-platform API and primitives described in the ECMAScript standard (JS API)

So it turns out that React Native should supply fetch, XMLHttpRequest and WebSocket APIs to JS runtime. Although, there are no such libraries that would work in appliance with WHATWG specifications (e.g. mimic web behaviour) for React Native target platforms (iOS and Android). That implies React Native should’ve re-implement existing standards and it did.

XMLHttpRequest

XMLHttpRequest (XHR) is an object that is used to interact with remote resources. It was originally designed by Microsoft long long time ago and later on adapted by other browser vendors. Despite the naming, nowadays this object is capable of transmitting different data types (not only XML) and work over various protocols based on TCP.

In React Native, XMLHttpRequest consists of two parts: front-end and back-end. The front-end part is something that you can interact with in JavaScript. It is capable of sending appropriate request data to the XHR back-end which is responsible for processing network request and using underlying mobile platform.

The back-end part of XMLHttpRequest is called Networking. It is an abstraction on top of iOS and Android operating systems that provides a unified API layer that front-end can work with. Implementation of this layer is different for each platform. While on iOS Networking module takes advantage of built-in URLSession, Android version works on top of third-party OkHTTP library that supplies a certain abstraction for low-level Java networking API.

React Native’s “Networking” module takes care of request data

Therefore, once developer make a request, it goes through React Native’s Networking module and underlying operating system right to the requested resource.

Flow of the XMLHttpRequest in React Native

Fetch

Fetch is a modern concept equivalent to XMLHttpRequest. It offers a lot of the same functionality as the XMLHttpRequest, but is designed to be more extensible and efficient. It operates Request and Response objects that can be re-used in the following requests.

In modern browsers, this API is a part of WindowOrWorkerGlobalScope mixin that extends global JavaScript scope, however in older browsers and React Native (in order to not to re-implement a new API for both platforms) it uses GitHub’s whatwg-fetch polyfill that relies on XMLHttpRequest implementation, so it implicitly uses React Native’s Networking module under the hood. That implies some nuances that we need to be aware of. For instance, one of the known Networking issues is impossibility of transferring any binary data over the wire that currently leads to workarounds (like working with base64 representation of binary data) and/or third-party solutions (like react-native-fetch-blob). You can learn more about these and other implications in the second part of my ReactNativeEU talk.

WebSocket

WebSocket is a communications protocol, providing full-duplex communication channels over a single TCP connection.

You might already use WebSockets for developing your React Native applications. Every time you start an app in development mode, it creates a WebSocket connection between packager and the app itself, so every time you edit your JS files (or other static assets), it sends a patch signal to the app so it can react to the changes.

As far as WebSocket is somewhat different from a standalone request, there is a separated module called WebSockets that performs as a back-end for the technology. Not hard to guess, that by analogy with XMLHttpRequest, WebSocket also has two different implementations across operating systems. Android is being consistent using OkHTTP library while iOS uses a self-written implementation on top of NSStream.

Networking issues

The second part of this article is dedicated to cover the most common networking issues in React Native. Debugging network requests, sending binary data over the wire and credentials management — today we’ll unveil them all.

Networking issues in React Native may be tricky to debug, but there is always a way ;)

Debugging network requests

If you ever developed a mobile application using React Native, you might notice that it exposes a great way of debugging JavaScript code — Chrome Developer Tools (CDT). By a couple of taps your JavaScript can be run in a Chrome service worker that provides a wide set of debugging tools. There is a way to log runtime data to console, profile memory, use breakpoints — almost everything you might need. The only one thing went missing — debugging network requests.

Trying to trace remote request using Chrome Developer Tools

Due to implementation specifics, CDT is not capable of handling network requests. If we consider web platform, XHR, fetch and WebSocket APIs are backed by the browser itself which gives it an advantage of having spies that makes it possible to trace network requests. However, React Native doesn’t use Chrome to make networking requests (all network requests goes through the Networking module no matter what), so it can’t trace them.

There is a way to avoid using React Native networking polyfill (so all requests in CDT will go through the Chrome platform and therefore will become traceable), but that would bring inconsistencies between your runtime environment in CDT and your app. For instance, fetch will become capable of sending binary data in CDT, but throw an error in RN app. This is a thin ice and therefore not recommended to use. Hopefully, there are many alternative ways you can use to debug network requests.

Using TCP traffic monitoring tools

This approach works if you develop using emulator. All your requests will go through your PC/Mac which means they will be regular HTTP requests* and therefore, become traceable.

By “regular HTTP requests” I mean HTTP requests that are similar to the requests we make in our browsers. HTTP protocol is based on TCP, so most of the traffic monitoring tools will be capable to trace them down.
Monitoring TCP connection

There is a way of using CharlesProxy, Wireshark or their analogs to get a list of outcoming TCP connections. One (or more) of these connections will be related to the app you’re trying to debug.

Monitoring TCP traffic becomes a little bit more complex when we start developing using real device. In order to monitor traffic from mobile, you need to configure a proxy (it is possible with tools like CharlesProxy (docs), mitmproxy (docs) and some others) and manually re-route application requests through it.

Using XHRInterceptor / WebSocketInterceptor

There is another way of tracing your remote requests: by using interceptors. React Native ships with two interceptors for XMLHttpRequest and WebSockets accordingly. Each one in meant to be used for tracing an appropriate API: XHRInterceptor for XMLHttpRequest and fetch requests, WebSocketInterceptor for WebSocket connections.

Using XHRInterceptor to trace XHR/fetch requests

These APIs are not documented or exposed as a part of the object, exported by react-native package. If you would like to use either of them, there is no other way than relying on current relative import path (which may change in upcoming versions of React Native).

Hopefully, you don’t have to do any of these by your own. There is a pretty great tool brought to us by Infinite Red called Reactotron. It is a swiss knife for React(+Native) development that provides lots of helpful functionality like debugging network requests, redux actions, sagas and much much more. You can easily find a full list of features on its website.

At this moment, the only solution React Native provides by default is “Networking monitor” that you can enable by shaking your device in development mode. However, the functionality of this tool is very limited to basic tracing of the request. Hopefully, there are lots of approaches in the community that may slightly improve your experience in this area.

Sending binary data over the wire

Another big topic related to networking issues is about transmitting binary data in React Native. It is not a secret that React Native never supported transferring files out of box. We use various workarounds like converting binary data to base64 strings or third-party solutions like react-native-fetch-blob, but the underlying issue stays unsolved. This part will unveil some internals of the File/Blob management, consider workarounds and potential solutions we can apply to React Native to bridge the gap.

Default JavaScript runtime doesn’t contain File/Blob constructors which makes it impossible to represent binary data. However, all modern browsers enhance a default runtime environment by adding their custom implementations so you can work with file inputs and binary data straight from JS. With these implementations actual binary data is stored on the browser/OS side and the only part which is exposed to developer is a reference wrapped into File/Blob object. Unfortunately, JS environment (and Networking module that exposes provides backend for XHR and fetch) of React Native doesn’t support either of these objects, so there is no “default” way of getting a reference to the file from your code.

Convert binary to base64

Base64 is a group of similar binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation.

As far as React Native is not capable of transferring non-primitive data over the bridge (so only serialisable data structures), the only way of getting there is to convert binary format to a string. Base64 sounds like a good solution until we consider a round-trip request:

Round-trip of a base64 request

Imagine we request an image from our server. Not just a URI, but the file itself. Request goes from JS thread through React Native platform to underlying OS and, finally, to the requested resource. That resource (your API, for instance) should respond with a base64 string that will be returned to JavaScript by replaying the request flow in reverse order.

Once response hits the bridge, JavaScript thread should allocate memory, sufficient for representing it. That implies certain performance issues that we want to avoid writing our React Native applications. Most of these problems are outlined in the great article of Tal Kol “Performance limitations of React Native and how to overcome them”.

Furthermore, we also need to render an image. Therefore we supply base64 string as a URI parameter for React Native’s <Image /> tag. At the moment of runtime, JSX tree that React Native compose will be sent to the native thread where the native UI lives, therefore the base64 string we supply will be traveling over the bridge again causing memory spikes and additional performance issues.

Now, when we considered potential issues of using base64, I’d like to focus your attention on the alternative approaches we can use for transmitting binary data in React Native.

Can we do better?

Of course, the problem outlined above is not new and probably every developer who ever had to transmit binary data faced it. This problem inspired many developers to give birth to numerous third-party solutions we can use. Although, the underlying issue is still not solved.

The only solid solution for this problem would be to bridge the missing bits of the standard. Hopefully, some time ago Satyajit Sahoo and Mike Grabowski from Callstack.io created a PR to that add these bits to the WebSocket module. After six months of discussions, rebases and reviews, it has been merged and from now on, the latest versions of React Native supports transmitting binary data over WebSocket protocol (🎉!). Although, it solves only a part of the problem: you still can’t send binaries using Networking module. This part of the issue is still under consideration, but you can keep an eye on the progress of the second PR here. Once it is merged, the problem of transferring binary data in React Native will be closed for good.

Conclusion

React Native is a complex platform in terms of composition of different patterns, libraries and architectural solutions. Network layer is only a one of many interesting topics that powers the platform, although for some reason not covered by the community at all. Hope this article shed some light on the network layer and pushed your knowledge of the platform a little bit further.

Have any questions left? You are very welcome to ask them on Twitter or by commenting this article below.

All these amazing illustrations drawn by Nicolas: shadow.x.q84@gmail.com.