Preventing memory leak by handling errors and request cancellations separately in Axios
Tags: axios, cancel, request, errors
Published: 2020-12-23 17:56:35

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.

```javascript
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;

```

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.

```javascript
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();
  };
}, []);
```

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.
```

## 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`.

```javascript
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.");
  };
}, []);
```

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.