DEV Community

Discussion on: useCancelToken: a custom React hook for cancelling Axios requests

Collapse
 
mostafaomar98 profile image
MostafaOmar98 • Edited

Hi, thanks for your article.

I believe that the currentuseCancelToken is faulty in some cases.

Currently: it exports a newCancelToken method that generates a new cancel token on each call of this function, and saves the latest cancel token source object on a ref. This will cause a problem in a component that does 2 different api calls with 2 different tokens. This hook will only cancel the latest of them on unmount.

i.e., a component that looks like this
function A() {
    const [x, setX] = useState<number>(0);
    const {newCancelToken, isCancel} = useCancelToken();
    const url = "https://httpbin.org/delay/3";

    useEffect(() => {
        (async () => {
            try{
                await axios.get(url, {
                    cancelToken: newCancelToken()
                });
                console.log("Done 1");
                setX(1);
            } catch(error) {
                if (isCancel(error)) console.log("canceled 1");
                else console.log(error);
            }
        })();

    }, []);


    useEffect(() => {
        (async () => {
            try{
                await axios.get(url, {
                    cancelToken: newCancelToken()
                });
                console.log("Done 2");
                setX(2);
            } catch(error) {
                if (isCancel(error)) console.log("canceled 2");
                else console.log(error);
            }
        })();

    }, []);


    useEffect(() => {
        (async () => {
            try{
                await axios.get(url, {
                    cancelToken: newCancelToken()
                });
                console.log("Done 3");
                setX(3);
            } catch(error) {
                if (isCancel(error)) console.log("canceled 3");
                else console.log(error);
            }
        })();

    }, []);

    return <div>Hey {x}</div>
}
Enter fullscreen mode Exit fullscreen mode

I've reproduced this bug in the code in this repo here: github.com/MostafaOmar98/use-cance...

When clicking unmount A quickly (before allowing the api calls to finish),
Expected result: logs to console "canceled 1", "canceled 2", "canceled 3" (order doesn't matter) with no warnings
Current result: it logs canceled 3, Done 1, Done 2, and spits out the setState warning

The suggested implementation is to create only one cancel token per component and cancel all api calls on unmounting:

export const useCancelToken = () => {
  const axiosSource = useRef<CancelTokenSource>(axios.CancelToken.source());
  const isCancel = axios.isCancel;

  useEffect(
    () => () => {
      axiosSource.current.cancel();
    },
    []
  );

  return { cancelToken: axiosSource.current.token, isCancel };
};
Enter fullscreen mode Exit fullscreen mode

Also, as an addition to the article. This hook only deals with cancelling api calls on unmounting. Another use case for cancelling is cancelling api calls on dependency changes. For example:

...
const userId = props.userId;
useEffect(() => {
  // load something based on userId
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

If the userId changes quickly, we might want to cancel the old ongoing request. I believe it is better to create the CancelToken/AbortController locally inside the effect and cancel/abort in the return function of the effect without the usage of extra hooks. Do you have another suggestion or pattern for doing this?

Thanks!

Collapse
 
tmns profile image
tmns

Hey Mostafa!

Thanks for the thorough reply and bringing up the issue about creating multiple cancel tokens within the same component. It's true that if you're going to be using it in several different calls like that you'd either want to just create one cancel token and export it directly (like you suggested) or create the cancel token once in the calling component (and storing it in a ref if it may rerender). I think your approach is the simplest as long as a function isn't specifically needed for some reason. I'll add a little note to the article pointing to your comment πŸ™Œ

As for your other point about the use cases of the hook. Yes, this is only about unmounting. There are maaaany cases where you may want to cancel a request, which may not even require a dependency change (e.g. network issues and race conditions). For those cases I've typically just handled them manually like you suggested, as they tend to be more difficult to easily abstract than the classic "if component unmounts." If you come up with a better way though let me know!

Have a great one!

Collapse
 
mostafaomar98 profile image
MostafaOmar98

Thanks for the reply and the feature on the article! Have a great one too!