Cancel async Axios GET request – React hooks

I have an Axios GET request that starts fetching an API when clicking a button. This request takes a long time (around a minute) and I would like to cancel it and start a new one when the button is clicked again.

Also, I would also like to be able to cancel the request if it is pending and I refresh the page.

I tried everything I found here on SO but nothing seems to work. I also read the Axios doc about cancelling : https://github.com/axios/axios#cancellation.

All this lead me there :

Updated code (see edit below)

const CancelToken = axios.CancelToken;
let cancel;

function App() {
    useEffect(() => {
        async function getPairs() {
            try {
                if (cancel) {
                    cancel();
                }

                const res = await axios.get('http://localhost:3000/binance/pairs?timeframe='+timeframe+'&strat='+strat, {
                    cancelToken: new CancelToken(function executor(c) {
                    cancel = c;
                    })
                });

                if (res.status === 200 || res.data.status === 'success') {
                    setPairs(res.data.pairs);
                    setReloadConfig(false);
                }
            }
            catch (err) {
                if (axios.isCancel(err)) {
                    console.log("cancelled");
                } else {
                throw err;
                }
            }
        }
    
        // reloadConfig is a React useState hook that is set to true when clicking a button 
        if (reloadConfig) {
            getPairs();
        }
    }, [reloadConfig]);
}

export default App;

Note that reloadConfig is a React useState hook that is set to true when clicking a button, therefore triggering the useEffect hook and the Axios request.

Problem is, if I click the button multiple times, no request is cancelled and a new one is created every time. I can see all the requests being treated in my terminal.

How can I cancel the last pending request and create a new one when clicking the button triggering the useEffect ?

Also, how can I cancel a pending request if I refresh the page ?

—– Edit —–

After twiking a bit with the code, I found out that placing cancelToken and cancel variables before the component function declaration made me move forward. Now, cancel is not undefined anymore and I get the cancelled message from the catch block :

catch (err) {
    if (axios.isCancel(err)) {
        console.log("cancelled");
    }
}

If I console.log(cancel), it returns this :

ƒ cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
}

So it tells me cancellation was successful (if I understand correctly ?), but I can still see all the requests being logged in the backend terminal and still being processed. What’s wrong with it ?

Thank you very much

—– Edit 2 —–

The solutions showed above all worked. The problem was a misunderstanding on my part : when I do a call to my API to fetch the results, the results are actually fetched by a library that itself fetches one URI per result, meaning that one call to my API makes multiple calls to another API via said library.

I can say the solutions showed above by the community worked because I tried them on a sandbox with a “normal” mock API. I think what happens in my case is that, the library I use with my API calls +200 URLs, so I guess one call was cancelled but not the other 199.

If anyone have a solution to cancel everything that would be great but I think that’s more of a backend situation than frontend one at this point.

Thanks to everyone for your help and patience.

Answer

Since you are depending on useEffect to fire up your request, you can use the cleanup function of useEffect to cancel your API request before a new one is executed.

function App() {
    useEffect(() => {
        let CancelToken = axios.CancelToken;
        let source = CancelToken.source();
        async function getPairs() {
            try {

                const res = await axios.get('http://localhost:3000/binance/pairs?timeframe='+timeframe+'&strat='+strat, {
                    cancelToken: source.token
                });

                if (res.status === 200 || res.data.status === 'success') {
                    setPairs(res.data.pairs);
                    setReloadConfig(false);
                }
            }
            catch (err) {
                if (axios.isCancel(err)) {
                    console.log("cancelled");
                } else {
                throw err;
                }
            }
        }
    
        // reloadConfig is a React useState hook that is set to true when clicking a button 
        if (reloadConfig) {
            getPairs();
        }

        return () => {
             source.cancel('Cancelled due to stale request');
        }
    }, [reloadConfig]);
}

export default App;

Note that you should define your cancel variable within useEffect otherwise it will be re-initialzed to undefined on next render if you directly define it within the component.