DEV Community

Cover image for Preventing memory leak by handling errors and request cancellations separately in Axios
Samet Mutevelli
Samet Mutevelli

Posted on • Originally published at samet.web.tr

Preventing memory leak by handling errors and request cancellations separately in Axios

I am currently working on a custom hook in a React application. The hook simply makes an API request to a third party API and fetches some data. My Axios call updates the state of the hook with some data or an error message, based on the response from the API call. I encountered a problem in the implementation and it took me a while to figure out. In this post, I am sharing my solution.

Problem

When making a network request with axios in a React app, you will likely update your state based on the response of the request. The data that comes in the .then() block is usually saved in the state, and the .catch() block usually updates the state with an error message.

const dataFetchingHook = () => {
  const [data, setData] = useState([]);
  const [error, setError] = useState("");

  useEffect(() => {
    axios
      .get("https://someapi.com/data")
      .then((response) => setData(response.data))
      .catch(() => setError("Error fetching data"));
  }, []);
};

return data;

Enter fullscreen mode Exit fullscreen mode

With this approach, it is assumed that every network request will be completed and resulted in either resolve or rejection. However, when the request is canceled halfway, the Promise also rejects and falls into the .catch() block.

To get to my point, now let's set up the network request so that it cancels if the component that uses my dataFetchingHook is unmounted.

useEffect(() => {
  const source = axios.CancelToken.source();

  axios
    .get("https://someapi.com/data", { cancelToken: source.token })
    .then((response) => setData(response.data))
    .catch(() => setError("Error fetching data"));

  return () => {
    source.cancel();
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

The problem resides right here: When the component is unmounted, the network request cancels but canceled network requests fall into the .catch() block of the request, just like errors. However, the .catch() block is trying to update our error state, and React tells us this is a memory leak:

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.
Enter fullscreen mode Exit fullscreen mode

Solution

axios provides a method to check if a rejection is caused by a network cancellation or not. We simply check inside the .catch() block whether the network request is canceled halfway or completed and rejected by the server.

Let's update the useEffect.

useEffect(() => {
  const source = axios.CancelToken.source();

  axios
    .get("https://someapi.com/data", { cancelToken: source.token })
    .then((response) => setData(response.data))
    .catch((error) => {
      if (axios.isCancel(error)) {
        console.log(error); // Component unmounted, request is cancelled.
      } else {
        setError("Error fetching data");
      }
    });

  return () => {
    source.cancel("Component unmounted, request is cancelled.");
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Here, I made use of the error argument of the callback inside the .catch() block. axios.isCancel(error) checks if the rejection is caused by a cancellation. Remember, inside useEffect's return statement, we canceled the network request if the component is unmounted.

Now, if there is a network cancellation, in the case of the user clicks somewhere and the data-fetching component is unmounted; the application is not trying to update the state with setError, it logs a different error instead.

Conclusion

Network requests that are made by axios are very easy to cancel. Simply make use of the CancelToken object and cancel the request if the component is unmounted, especially if you are updating your state inside the .catch() block.

Top comments (0)