Writing a React hook to cancel promises when a component unmounts.

Rajesh Naroth
4 min readJan 22, 2019

--

CC0 Image from https://jaymantri.com/

While writing SPAs with multiple pages, you will come across this error:

Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.

This is because you switched views while an async operation such as an XHR promise was in progress. By the time this promise was resolved, your view has already been unmounted.

Attempts to solve this by using isMounted() is discouraged. React team calls it an anti-pattern. Quoting from the site:

The primary use case for isMounted() is to avoid calling setState() after a component has unmounted, because calling setState() after a component has unmounted will emit a warning. The “setState warning” exists to help you catch bugs, because calling setState()on an unmounted component is an indication that your app/component has somehow failed to clean up properly. Specifically, calling setState() in an unmounted component means that your app is still holding a reference to the component after the component has been unmounted - which often indicates a memory leak!

Thus isMounted() is being deprecated.

They recommend you write cancelable promise. A recipe is provided in the same page based on this. It is good enough.

Now you can create a higher order component use it to cancel your pending promises when it unmounts. The only problem is you have to warp every container/component that needs this behavior with the HOC. A bit messy.

CodeSandbox:

You can see this issue and the solution in action in the codesandbox below. Open the console tab to watch the logs and React errors:

https://codesandbox.io/s/cancelable-promise-tev18?file=/components/helpers/useCancelablePromise.js

Hooks to the rescue

I love how hooks let you write clean React code. Here is how you would use a cancelable promise hook inside a functional component or another action hook. We’ll build this hook a bit further down.

const { cancellablePromise } = useCancellablePromise();const data = await cancellablePromise(getJson(url));

That’s it. The cancelable promise hook will take care of the clean up when the component is unMounted. So how do we create this magical hook? Quite simple actually. First you must write a function to create a cancelable promise. This is derived from the react site.

export function makeCancelable(promise) {
let isCanceled = false;
const wrappedPromise =
new Promise((resolve, reject) => {
promise
.then((val) => (isCanceled ? reject({ isCanceled }) : resolve(val)))
.catch((error) => (isCanceled ? reject({ isCanceled }) : reject(error)));
});
return {
promise: wrappedPromise,
cancel() {
isCanceled = true;
},
};
}

You are essentially wrapping a promise around the original promise. Now we write a hook that keeps track of all the promises that you have created so far and cancels them when it is no longer needed. I’ve added explanations in comments here.

function useCancellablePromise() {  // think of useRef as member variables inside a hook
// you cannot define promises here as an array because
// they will get initialized at every render refresh
const promises = useRef(); // useEffect initializes the promises array
// and cleans up by calling cancel on every stored
// promise.
// Empty array as input to useEffect ensures that the hook is
// called once during mount and the cancel() function called
// once during unmount
useEffect(
() => {
promises.current = promises.current || [];
return function cancel() {
promises.current.forEach(p => p.cancel());
promises.current = [];
};
}, []
);
// cancelablePromise remembers the promises that you
// have called so far. It returns a wrapped cancelable
// promise
function cancellablePromise(p) {
const cPromise = makeCancelable(p);
promises.current.push(cPromise);
return cPromise.promise;
}
return { cancellablePromise };
}

useEffect() is the most magical hooks of all. This is what removes the clutter from your react code. Along with useState(), it weans your React code away from classes. No more mount/unmount related code. You can compose your hooks within the component or other hooks.

Using a cancelable promise, you MUST use .catch() and check if the error resulted from a canceled promise or not. If you always want to suppress errors from a canceled promise, you can modify the makeCancelable function like so:

export function makeCancelable(promise) {
let isCanceled = false;
const wrappedPromise =
new Promise((resolve, reject) => {
// Suppress resolution and rejection if canceled
promise
.then((val) => (!isCanceled && resolve(val)))
.catch((error) => (!isCanceled && reject(error)));
});
return {
promise: wrappedPromise,
cancel() {
isCanceled = true;
},
};
}

If you are curious about how to manage state in your React app using hooks, checkout this article:

--

--