Cancellable promises in react and why is it required

Praveenkumar
Jul 04, 2020

Broken Promise

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!

reactreact-hookscancellable-promise

WRITTEN BY

Praveenkumar

I am a passionate full stack developer. I develop primarily using JS specifically TS. I also work on JAVA and python. I like exploring latest languages, technologies and frameworks.