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?: boolean

If 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:

Knowing this, we can override the send method of XMLHttpRequest to:

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.

Want to level up your React skills?

Sign up below and I'll send you content just like this about React straight to your inbox every week.

No spam, promise. I hate it as much as you do!

, enjoying the article? Now think of 3 friends who are interested in React, React Native and would be into it, and share the link with them! 👇

https://goshakkk.name/react-native-activity-indicator-fetch/