How To Avoid Memory Leaks In React

How To Avoid Memory Leaks In React

Screen Shot 2022-06-14 at 12.42.17 pm.png When I started using react hooks a few years back, I noticed memory leaks occuring but didn't really know how to prevent them until I did a bit of digging. It turns out the solution is nice and easy to fix and the implementation is super simple.

1. The Problem

Memory leaks happen in react when you have unmounted a component, but that component is subscribed to something. The subscription could be a DOM event listener, an API call or a WebSocket subscription.

So typically you would have something like a fetch request within your useEffect hook, which asynchronously fetches some data and then usually you set your state as soon as that data has been received. But what happens if a user has a slow internet connection, and navigates away from the current component before the request has finished? React will set the state on the unmounted component, causing a memory leak.

2. Introducing AbortController

The AbortController is an interface that allows us to easily abort web events.

const abortController = new AbortController();

It contains an object named signal and an abort() method for aborting.

abortController.signal;
abortController.abort();

The signal object contains three properties, by default these are

{
  aborted: false,
  onabort: null,
  reason: undefined
}

Calling abortController.abort(); will abort any DOM request before it has completed. This method changes the properties of the abortController.signal object, and causes any fetch() promise to reject with a DOMException called AbortError . After calling the abort method, the updated abortController.signal object will now look more like this:

{
  aborted: true,
  onabort: null,
  reason: DOMException: signal is aborted without reason
}

developer.mozilla.org/en-US/docs/Web/API/Ab..

3. How to use AbortController with a fetch request

As you probably know, fetch requests take an optional second parameter where you can specify a bunch of different "options". The most common options you've probably seen:

{
  method: 'POST',
  cache: 'no-cache'
  headers: {
      'Content-Type': 'application/json'
  }
}

It turns out, one of the available options we have is a signal option. This is where the abortController.signal object comes to use. By simply adding it to the options of the fetch request, the abortController can work it's magic.

fetch('example.com', { signal: abortController.signal })

developer.mozilla.org/en-US/docs/Web/API/fe..

3. Implementing a Solution

So we now know how to use the AbortController interface, but how do we use it within the react useEffect hook? The react useEffect hook accepts a cleanup function, which will be invoked whenever the component unmounts. Let's take a look at the example below to see how it works.

const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
  const abortController = new AbortController();
  const fetchData = async () => {

    const data = await fetch('example.com', { signal: abortController.signal });
    const result = await data.json();
    setPosts(result.data);
    setIsLoading(false);
  }
  fetchData();
  // This will cancel the request right before the component unmounts.
  return () => abortController.abort();
}, [])

4. Error Handling

Now that we know how to implement the basics, let's take it a step further and make sure we don't cause a memory leak when trying to handle errors. We will need to access the returned error message to conditionally set the error state, to again avoid setting the state on the unmounted component. As I mentioned earlier, the abort function causes the fetch request to throw an error called 'AbortError'. This is how we can use this error message:

const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  const abortController = new AbortController();
  const fetchData = async () => {
    try {
    const data = await fetch('example.com', { signal: abortController.signal });
    const result = await data.json();
    setPosts(result.data);
    isLoading(false);
  } catch (error) {
    // This will prevent setting the state after unmounting
    if (error.name !== 'AbortError') {
      setIsLoading(false);
      setError(error.message);
    }
    console.log(error);
  }
  fetchData();
  return () => abortController.abort();
}, [])

5. Conclusion

The main points to take from this article are:

  1. We can use the AbortController interface to easily abort fetch requests.
  2. The Fetch API can accept a signal as one of the properties of the options parameter, so the combination of the abort() function and the signal object is what makes this work.
  3. In the useEffect hook, we can use the built in cleanup functionality so that we only ever call the abort() when unmounting a component.

I hope you enjoyed this article. Please feel free to leave me a comment below. Have a great day :)

Did you find this article valuable?

Support Dominic Duke by becoming a sponsor. Any amount is appreciated!