Cancellable promises in react and why is it required
Praveen
Posted on July 18, 2020
Prerequisite:
- Basic understanding of react component and react hooks.
- Basic understanding on how the promises work.
- Basic knowledge of Typescript.
I will be using react with typescript (tsx) for examples. However, the same code can be used in js files without the type definitions.
Promises once fired, cannot be cancelled. So cancelling the promises in our current context means to ignore the result of the promise. When an api call is fired inside a react component, any state update after the component is destroyed (inside the then block of the promise), will cause error.
In order to avoid this, lets write a function that takes a normal promise and converts that into a cancellable promise which does not invoke any state updates on the component that is destroyed.
export const cancellablePromise = (promise: Promise<any | void>) => {
const isCancelled = { value: false };
const wrappedPromise = new Promise((res, rej) => {
promise
.then(d => {
return isCancelled.value ? rej(isCancelled) : res(d);
})
.catch(e => {
rej(isCancelled.value ? isCancelled : e);
});
});
return {
promise: wrappedPromise,
cancel: () => {
isCancelled.value = true;
}
};
};
Lets break down the above code and try to understand whats happening.
export const cancellablePromise = (promise: Promise<any | void>) => {
This function takes a single argument, which is the promise that needs to be cancelled when the component that is using the promise is destroyed.
const isCancelled = { value: false };
We declare an object that holds a value that states whether the current promise is cancelled or not.
This value will be changed to true
when the component is destroyed, to indicate that the promise is cancelled.
const wrappedPromise = new Promise((res, rej) => {
promise
.then(d => {
return isCancelled.value ? rej(isCancelled) : res(d);
})
.catch(e => {
rej(isCancelled.value ? isCancelled : e);
});
});
Here, we create one more promise, a wrapped one, that takes the original promise and rejects if the promise is cancelled (value
is true
). If the promise is not cancelled (value
is false
), only then the result is resolved. Also, when there is some error in the original promise, then that error is reported to the caller only if the promise is not cancelled. If the promise is cancelled then the cancellation is reported.
return {
promise: wrappedPromise,
cancel: () => {
isCancelled.value = true;
}
};
Finally, we return an object, which has a converted promise as promise
and another function called cancel
which can be called, to cancel the promise when the component is destroyed.
Now lets see how to use this function in a react component. Consider the following component.
const HelloComponent = () => {
const [message, setMessage] = useState('');
useEffect(async () => {
const p = await axios.get('https://<someurlreturningthemessage>').then(d => d.data);
const { promise, cancel } = cancellablePromise(p); // converting original promise to cancellable promise
promise.then(d => {
// Write your local state updates here
setMessage(d);
});
return cancel;
}, [setMessage]);
return <div>
<h2>{message}</h2>
</div>;
};
The above component just displays the message that is returned from an api call (a promise). Here, i have used an axios
get call to get the message. The useEffect
react hook is used to update the state of the local component based on any modifications in the list of dependencies that is passed as the second parameter, here it is just the setMessage
function.
The main thing about the useEffect
hook is that, the function that is returned with in the useEffect
hook is called when the component is destroyed or removed from the component tree. As you can see, we return the cancel
function that we got from the cancellablePromise
. When react calls cancel
function, the promise is set to cancelled (value = true
). Now, the then block will not be called (which contains the state updates) if the component is destroyed.
Refer here for more detail about the useEffect
hook.
Now the cancellablePromise
can be used inside any component where there is an api call or any other promise related stuffs.
Conclusion:
It is generally a good practice NOT to call the apis directly from the component. You may have to use some sort of state container libraries like redux, to do that for you and inject the result as props to your component. However, there may be exceptional cases where you need to call the apis directly (eg. Autofill). In such cases you can use the cancellablePromise
.
Hope you enjoyed! Happy Hacking!
Posted on July 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.