• 1053 words

Aborting requests in React Native

How can we close an active connection initiated with Fetch? And most important, what do we mean by "closing a connection"?

Gate with hanging "closed" sign
Photo by Masaaki Komori / Unsplash

Update

This post is now, happily, outdated since the AbortController implementation has been included in React Native 0.60.0 (comment here)

I’m doing this post since there is a lot of confusion going on around the React Native (and web too actually) community around the matter of “canceling a request” and many people asked me through a Github issue to clear up the matter.

What do we mean by “closing a connection”?

This is really an important thing to understand.

  1. One thing is the interface exposed by the method used to request something
  2. And one thing is the actual method used to request something

Why this differentiation?

We know that Fetch exposes its functionalities through Promises; we easily know how to reject a Promise and doing so, in the case of an HTTP request, we “shut the door” to every future data that may come from that Promise. But what is the actual meaning of just rejecting the Promise that initiated the connection? It means that the connection will continue to live and, potentially, download lots of data in the background. This is because rejecting a promise operates just with Fetch’s Promise but not with the HTTP request itself that won’t stop the work it is doing (examples and demonstrations later on).

When we’re talking about a few kilobytes (a JSON response that we’re waiting from a REST API for example) this is completely fine, and in that case, rejecting the Promise without stopping the real connection will be enough. This solution has come to the surface especially in relation to a famous issue about calling setState on an unmounted component. This issue has to do with the React internals and it’s pretty easy to understand, also a lot of blogs talk about it. Anyway, this thing we just talked about may not affect you depending on how you wrote your code (for example you will not notice this problem if you use Redux).

But what if we’re talking about the download of a heavier kind of data? This is the exact situation where I was during the development of an enterprise application in React Native. Imagine the UI showing a button to the user, while he’s in a mobile application, in a PWA, or in a responsive website from a smartphone, that says “start downloading data” and then, when he presses that button, the text of it changes with “stop downloading data” and he presses that button again. What do we do? If we’re just rejecting the Promise it will continue to download those data since, as we said, rejecting the promise doesn’t result in a terminated connection. As a matter of fact, in this way, we potentially expose the user to pay extra money for his mobile carrier contract and this is something we want absolutely to avoid.

So how can we tell Fetch to stop the connection and let the network module of our device rest?

What the specs say

If you ask me, the Fetch API is good but not that much. Not to be rude, but it seems like they almost forgot to design something that could be production-ready. So long story short, after a first 2015 “abort functionality” request, they successfully introduced it in 2017. Here are the MDN specs and here is Jake Archibald talking about it in google developers.

Shortly this is an example of a canceled request through the abort method (I will not go into details here since it isn’t in the scope of this article):

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal })
  .then((response) => {
    return response.text();
  })
  .then((text) => {
    console.log(text);
  });

Really cool, isn’t it? When I discovered this I ran into my code editor to edit my “API service” that wraps and enhances fetch and implemented it. Started the React Native project I was trying to abort my requests but nothing was happening (not even an error), so I immediately opened an issue on the React Native Github repo. Unfortunately, as I said at the beginning of this article, there is a lot of confusion about this issue and on that GitHub thread, there are a lot of wrong explanations (mines included).

Polyfills, fake things that pretend to be true

I didn’t stop there. Noticing that it wasn’t working, I started trying available polyfills on NPM and they were actually (not) working. At first, I wasn’t noticing that the connections weren’t killed, rather the polyfills were wrapping the original window.fetch and rejecting the promise… they were doing the exact same thing I explained above and that you can easily do with ten/fifteen lines of code (actually these lines of code are the exact ones that are showed in the Facebook blog post I linked above).

XHR.abort()? Can this be done?

Well, it turns out that the XHR obscene syntax had an abort method used to terminate connections. While this could be a good approach to support old browsers when you’re doing a web app, this isn’t a viable option in React Native since only fetch is baked inside the core.
Actually, I was wrong. As specified in this doc page XHR is backed into the React Native core and can be used to abort the connection.
The problem is that the fetch API is really cleaner than the XHR one and furthermore the migration from fetch to XHR may not be simple for certain apps (and honestly it seems a step back).

So are polyfills useless in React Native?

Yes, Yes, and Yes. There will be no possible polyfills for this feature if the React Native core team doesn’t implement this first. The only thing that it’s actually possible is to build a native module (or search for an existing one) that implements this feature on the native side too. Someone talked about this in an rn-fetch-blob issue but it’s such a core functionality that I’d not rely on an external lib to accomplish it.

Summing up

In React Native, right now, the best you can do is to use the trick shown in the previous Facebook blog link and it’s something like this:

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      (val) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
      (error) => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};