4

What is the correct approach to cancel async requests within a React functional component?

I have a script that requests data from an API on load (or under certain user actions), but if this is in the process of being executed & the user navigates away, it results in the following warning:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Most of what I have read solves this with the AbortController within the componentDidUnmount method of a class-based component. Whereas, I have a functional component in my React app which uses Axois to make an asynchronous request to an API for data.

The function resides within a useEffect hook in the functional component to ensure that the function is run when the component renders:

  useEffect(() => {
    loadFields();
  }, [loadFields]);

This is the function it calls:

  const loadFields = useCallback(async () => {
    setIsLoading(true);
    try {
      await fetchFields(
        fieldsDispatch,
        user.client.id,
        user.token,
        user.client.directory
      );
      setVisibility(settingsDispatch, user.client.id, user.settings);
      setIsLoading(false);
    } catch (error) {
      setIsLoading(false);
    }
  }, [
    fieldsDispatch,
    user.client.id,
    user.token,
    user.client.directory,
    settingsDispatch,
    user.settings,
  ]);

And this is the axios request that is triggered:


async function fetchFields(dispatch, clientId, token, folder) {
  try {
    const response = await api.get(clientId + "/fields", {
      headers: { Authorization: "Bearer " + token },
    });

    // do something with the response

  } catch (e) {
    handleRequestError(e, "Failed fetching fields: ");
  }
}

Note: the api variable is a reference to an axios.create object.

ford04
  • 66,267
  • 20
  • 199
  • 171
Sheixt
  • 2,546
  • 12
  • 36
  • 65

3 Answers3

8

To Cancel a fetch operation with axios:

  1. Cancel the request with the given source token
  2. Ensure, you don't change component state, after it has been unmounted

Ad 1.)

axios brings its own cancel API:

const source = axios.CancelToken.source();
axios.get('/user/12345', { cancelToken: source.token })
source.cancel(); // invoke to cancel request

You can use it to optimize performance by stopping an async request, that is not needed anymore. With native browser fetch API, AbortController would be used instead.

Ad 2.)

This will stop the warning "Warning: Can't perform a React state update on an unmounted component.". E.g. you cannot call setState on an already unmounted component. Here is an example Hook enforcing and encapsulating mentioned constraint.


Example: useAxiosFetch

We can incorporate both steps in a custom Hook:

function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
  React.useEffect(() => {
    const source = axios.CancelToken.source();
    let isMounted = true;
    axios
      .get(url, { cancelToken: source.token })
      .then(res => { if (isMounted) onFetched(res); })
      .catch(err => {
        if (!isMounted) return; // comp already unmounted, nothing to do
        if (axios.isCancel(err)) onCanceled(err);
        else onError(err);
      });

    return () => {
      isMounted = false;
      source.cancel();
    };
  }, [url, onFetched, onError, onCanceled]);
}

import React from "react";

import axios from "axios";

export default function App() {
  const [mounted, setMounted] = React.useState(true);
  return (
    <div>
      {mounted && <Comp />}
      <button onClick={() => setMounted(p => !p)}>
        {mounted ? "Unmount" : "Mount"}
      </button>
    </div>
  );
}

const Comp = () => {
  const [state, setState] = React.useState("Loading...");
  const url = `https://jsonplaceholder.typicode.com/users/1?_delay=3000&timestamp=${new Date().getTime()}`;
  const handlers = React.useMemo(
    () => ({
      onFetched: res => setState(`Fetched user: ${res.data.name}`),
      onCanceled: err => setState("Request canceled"),
      onError: err => setState("Other error:", err.message)
    }),
    []
  );
  const cancel = useAxiosFetch(url, handlers);

  return (
    <div>
      <p>{state}</p>
      {state === "Loading..." && (
        <button onClick={cancel}>Cancel request</button>
      )}
    </div>
  );
};

// you can extend this hook with custom config arg for futher axios options
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
  const cancelRef = React.useRef();
  const cancel = () => cancelRef.current && cancelRef.current.cancel();

  React.useEffect(() => {
    cancelRef.current = axios.CancelToken.source();
    let isMounted = true;
    axios
      .get(url, { cancelToken: cancelRef.current.token })
      .then(res => {
        if (isMounted) onFetched(res);
      })
      .catch(err => {
        if (!isMounted) return; // comp already unmounted, nothing to do
        if (axios.isCancel(err)) onCanceled(err);
        else onError(err);
      });

    return () => {
      isMounted = false;
      cancel();
    };
  }, [url, onFetched, onError, onCanceled]);
  return cancel;
}
ford04
  • 66,267
  • 20
  • 199
  • 171
  • Sorry for the delay in getting back to this! I have accepted @ford04 answer as it encompasses the specifics for Axios. – Sheixt Jul 23 '20 at 08:40
  • @theblackgigant answer is also helpful for those within the react component hooks approach for lifecycle management – Sheixt Jul 23 '20 at 08:40
  • this example always requests the data as soon as the component is mounted. What approach would you use for when the request is triggered by a user action (i.e. button click)? – Sheixt Jul 23 '20 at 09:11
  • You could directly invoke `axios.get(...)` inside the event handler, add a mutable ref (`useRef`), that stores information about the mount status (`isMounted`, same logic as above) and the cancel token `source`. If the component unmounts, invoke `source.cancel();` in a separate `useEffect`. – ford04 Jul 23 '20 at 17:11
  • Sandbox not found https://codesandbox.io/s/so-62533417-pxsgd – dipenparmar12 Nov 25 '21 at 04:56
  • Starting in `v0.22.0` the axios `CancelToken` is deprecated and shouldn't be used. Now the expectation is to use the native `AbortController`. See more information in [Axios's docs](https://axios-http.com/docs/cancellation). – technogeek1995 Oct 26 '22 at 14:17
3

useEffect has a return option which you can use. It behaves (almost) the same as the componentDidUnmount.

useEffect(() => {
  // Your axios call

  return () => {
    // Your abortController
  }
}, []);
Reyno
  • 6,119
  • 18
  • 27
0

You can use lodash.debounce and try steps below

Stap 1:

inside constructor:
this.state{
 cancelToken: axios.CancelToken,
 cancel: undefined,
}
this.doDebouncedTableScroll = debounce(this.onScroll, 100);

Step 2: inside function that use axios add:

if (this.state.cancel !== undefined) {
                cancel();
            }

Step 3:

 onScroll = ()=>{
    axiosInstance()
         .post(`xxxxxxx`)
              , {data}, {
                  cancelToken: new cancelToken(function executor(c) {
                         this.setState({ cancel: c });
                        })
                   })
                    .then((response) => {
    
                         }