Cancellable promises in react and why is it required

praveenkumarrr

Praveen

Posted on July 18, 2020

Cancellable promises in react and why is it required

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;
        }
    };
};
Enter fullscreen mode Exit fullscreen mode

Lets break down the above code and try to understand whats happening.

export const cancellablePromise = (promise: Promise<any | void>) => {
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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);
        });
});
Enter fullscreen mode Exit fullscreen mode

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;
    }
};
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
praveenkumarrr
Praveen

Posted on July 18, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024

Modern C++ for LeetCode 🧑‍💻🚀
leetcode Modern C++ for LeetCode 🧑‍💻🚀

November 29, 2024