Aug 28, 2019

Clean up request in useEffect React Hook

blogpost author photo
Tomasz Bernaciak
blogpost cover image
Creating a React application which will communicate with an external API is a piece of pie.

However, sometimes you may get this warning message:

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

The message is not very clear and it does not give you information about what actually needs fixing, especially when you see your application is working correctly. To visualize the problem let's imagine a simple application. It will have two buttons and two components. You click the first button and you render the first component, while clicking on the second button renders the second component. Now add an API connection - every component needs to make an API call to display data. The error occurs when you click the first button and immediately click the second one without waiting for the first component to render. In this way, you can make a request but the component gets unmounted before it is updated it. You need to clean it up after unmount - so you need to cancel the request.


const URL = "https://randomuser.me/api/";
const [data, setData] = useState(null);
 
useEffect(() => {
  fetch(URL)
    .then(results => results.json())
    .then(resp => { setData(resp.data)})
}, [URL]);

I created a simple React Hook for calling Random User API. When user information is fetched from API it will be stored in the local state as `data`. If you use it in your component there is a possibility that you will see the error above on component unmount. Let's see how to prevent this.

Clean up with AbortController

You can use AbortController. AbortController allows you to abort one or more DOM requests as and when desired. It is a browser API and can be used without importing any library. See the example below. I added a new `abortController` inside `useEffect` hook. Fetch allows to pass the second argument, and I send the `signal` instance as the second parameter. It can be used to abort a DOM request. The `useEffect` hook allows using a cleanup function. Anytime the effect is no longer valid, for example when a component using that effect is unmounting, this function is called to clean everything up. In our case, it is very helpful. Changing from one component to another will unmount the first one. During unmounting the component `useEffect` will call `abort()` method from `AbortController` to tell `fetch` and later the browser that this request is not needed anymore and can be canceled.


const URL = "https://randomuser.me/api/";
const [data, setData] = useState(null);
 
useEffect(() => {
  const abortController = new AbortController();
 
  fetch(URL, { signal: abortController.signal})
    .then(results => results.json())
    .then(resp => { setData(resp.data)})
 
  return () => {
    abortController.abort();
  };
}, [URL]);

Clean up with axios CancelToken

If you are using Axios, I also have a similar solution. It is a very popular Http client library used very often in React projects and it provides a request canceling mechanism. In the very beginning, I created a cancel token using `CancelToken.source` factory. I added the token to Axios request. Just like in the previous example, `useEffect` allows returning another function when effect fallback is not valid anymore. When a component is changed to another one, it will unmount the first causing `useEffect` to send a cancel signal using `source.cancel()`. Because it is canceled during the time a request is already started, an error will be caught in `try...catch` block. Axios provides a method to check if an error was caused by request cancellation or a different error occured.


const URL = "https://randomuser.me/api/";
const [data, setData] = useState(null);
 
useEffect(() => {
  const source = axios.CancelToken.source();
 
  const loadData = async () => {
    try {
      const response = await Axios.get(URL, {
        cancelToken: source.token
      });
      setData(response.data);
    } catch (error) {
      if (Axios.isCancel(error)) {
        // request cancelled
      } else {
        throw error;
      }
    }
 
  };
 
  loadData()
 
  return () => {
    source.cancel();
  };
}, [URL]);

Both presented examples do the same thing. Your choice of the solution should depend on how you do Http request in your application. Axios also provides an additional method to check if a received error has occurred because of request cancellation. To verify if a selected solution is working, just play with it. Unmount your component before receiving the response and perhaps add a timeout for the request to see it better. You should see a canceled request in the Network tab in your browser. If you have any questions, please ping me in the comments.