How to show an iOS network activity indicator when a request is in progress?
12 Jul 2017
If you've used an iPhone even for a few hours, you'd noticed that when the app is loading something, you would usually see a small spinner in the status bar.
You may think iOS does that automatically when it sees a network request, but in your own React Native app, you don't see that spinner.
You know your iOS users are going to expect to see it, but how in the world do you show it?
Digging through React Native docs, you have come across the <StatusBar />
component.
Among other things, you see something that would probably help:
networkActivityIndicatorVisible?
: booleanIf the network activity indicator should be visible.
Cool.
You can now set this property to true
to show it.
Except... how do you know when to show it?
Here is where it gets trickier.
You now have to keep track of the requests you make, somehow.
The most obvious option that comes to your mind is, add some additional code before you fetch
, and some more after the fetch is finished.
trackRequestStarted(); // NEW
fetch(...).then(() => {
trackRequestEnded(); // NEW
// your code...
});
We don't yet know what this code for tracking would be, but... does the above strike you as pretty, in any way? Does the prospect of having to wrap each of your fetches like this seem reasonable? I know, right?
Now, if there only were way to somehow extend the way fetch
works... so that we could track when a request starts and ends automatically.
To do that, though, we need to know a biit more about how fetch
in JavaScript works.
Essentially, it's just a convenient wrapper over XMLHttpRequest
, a low-level primitive for making network requests in JavaScript.
const req = new XMLHttpRequest();
req.addEventListener("load", onResponse);
req.addEventListener("error", onError);
req.open("GET", "http://example.org/");
reeq.send();
The above is equivalent to:
fetch("http://example.org/").then(onResponse, onError);
We don't have to know all the details, what matters is:
- An instance of
XMLHttpRequest
does not represent a real request until you call the.send()
method on it. XMLHttpRequest
communicates it status (load/error) using event listeners.
Knowing this, we can override the send
method of XMLHttpRequest
to:
- call
trackRequestStarted()
; - call
trackRequestEnded()
when the request either finishes successfully or errs.
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
trackRequestStarted();
this.addEventListener("load", trackRequestEnded);
this.addEventListener("error", trackRequestEnded);
origSend.apply(this, arguments);
};
This technique is known as "monkey patching". It means we are extending an object's (or, in this case, a prototype's) function.
We do so by "saving" the original function (in this case, send
), then redefining the send
of XMLHttpRequest
with our custom code to track when a request starts or ends, and finally calling the original function in the end.
If this still feels a bit hard to follow, David Walsh has a great post on basics of monkey-patching that I recommend reading.
Now, some pieces of the puzzle are still missing.
How do trackRequestStarted
and trackRequestEnded
work?
Well, to be able to answer the question of Is any request currently in progress?, we can keep a counter of active requests. We will increment it by 1 when sending a new request, and decrement it by 1 when the request finishes.
let activeRequests = 0;
function trackRequestStarted() {
activeRequests += 1;
}
function trackRequestEnded() {
activeRequests -= 1;
}
So if the counter is greater than 0, there are some requests in progress. And if it's zero, there are none.
Now we can kinda track whether any request is in progress.
The question becomes, how do we tie this with the StatusBar
component that we want to display?
It's clear that we would want to "ping" the root React component (the one that renders StatusBar
) every time the counter is changed, to let it know the current status.
Can we make this "tracker" an event emitter of sorts?
If we could, we would be able to subscribe for updates like this in the root component:
import React, { Component } from "react";
import { View, StatusBar } from "react-native";
import subscribeRequestStatus from "./fetch-status";
class Main extends Component {
state = { anyRequestInProgress: false };
componentDidMount() {
subscribeRequestStatus(this.handleRequestStatusChange);
}
handleRequestStatusChange = (isAnyInProgress) => {
this.setState({ anyRequestInProgress: isAnyInProgress });
};
render() {
return (
<View>
<StatusBar networkActivityIndicatorVisible={this.state.anyRequestInProgress} />
</View>
);
}
}
Implementing subscribeRequestStatus
is pretty trivial: we add the function to an array of 'listeners', which are called every time a request is initiated or finished.
The full code of the module would be:
// fetch-status.js
const listeners = []; // NEW
let activeRequests = 0;
// `fn` will be called every time a request is made or finished
// it will receive `true` if there is any request in progress,
// and `false` otherwise
export default function subscribeRequestStatus(fn) {
// NEW
listeners.push(fn);
}
function notifyListeners() {
listeners.forEach((fn) => fn(activeRequests > 0));
}
function trackRequestStarted() {
activeRequests += 1;
// NEW: notify listeners each time a new request is started
notifyListeners();
}
function trackRequestEnded() {
activeRequests -= 1;
// NEW: notify listeners each time a request finishes
notifyListeners();
}
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
trackRequestStarted();
this.addEventListener("load", trackRequestEnded);
this.addEventListener("error", trackRequestEnded);
origSend.apply(this, arguments);
};
This fetch-status.js
module is reusable.
You can copy it across your projects, and use like shown above with the root component example.
And now, your iOS users will feel more at home.