Correct way to cancel async axios request in a React functional component

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.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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;
}

Method 2

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

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

  return () => {
    // Your abortController
  }
}, []);

Method 3

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) => {
    
                         }


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x