Avoid Memory Leak With React SetState On An Unmounted Component
Tags: react, javascript
Published: 2019-11-14 16:45:20

---
title: Avoid Memory Leak With React SetState On An Unmounted Component
published: true
tags: React,JavaScript
canonical_url: https://www.rockyourcode.com/avoid-memory-leak-with-react-setstate-on-an-unmounted-component/
---


Raise your hand ✋, if you've seen this error in your React application:

```terminal
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
```

## The Problem

This error often happens when you make an **asynchronous request for data, but the component unmounts**. For example, some logic in your app tells React to navigate away from the component.

You still have a **pending request** for remote data, but when the data arrives and modifies the component's state, the app already renders a different component.

From the [React blog][antipattern]:

> The “setState warning” exists to help you catch bugs, because calling `setState()` on an unmounted component is an indication that your app/component has somehow failed to clean up properly. Specifically, calling `setState()` in an unmounted component means that your app is still holding a reference to the component after the component has been unmounted - which often indicates a memory leak!

In this post I'll show some possible workarounds for **avoiding memory leaks with data fetching**.

## Why Is This Happening?

When you fetch data, you make an asynchronous request. You normally do this by using a Promised-based API, for example, the browser-native **[`fetch`][fetch]**.

Example: Call to an API with `fetch` (Promise-based)

```javascript
function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    const fetchData = () => {
      setState(prevState => ({ ...prevState, isLoading: true }))

      fetch('https://ghibliapi.herokuapp.com/people')
        .then(response => response.json())
        .then(jsonResponse => {
          setState(prevState => {
            return {
              ...prevState,
              isLoading: false,
              loadedData: [...jsonResponse],
            }
          })
        })
        .catch(_err => {
          setState(prevState => {
            return { ...prevState, isLoading: false, isError: true }
          })
        })
    }

    // calling the function starts the process of sending ahd
    // storing the data fetching request
    fetchData()
  }, [])

  return <JSX here />
}
```

You could re-write the data-fetching to use **async/await**, but that's still a JavaScript Promise under the hood.  
JavaScript is [single-threaded, so you can't avoid "parking" your code when you do something asynchronous][color]. And that's why you either need event listeners, callbacks, [promises][promise], or async/await.

**The problem is that [you can't cancel a Promise.][cancelpromise]**

Now, your app might change the view, but the promise isn't fulfilled yet. **You can't abort the data fetching process** after you've started it.  
Thus, the above error happens.

## Typical Solutions Offered by Internet Searches

1. Use a third-party library like [bluebird][bluebird] or [axios][axios].

    **Problem**: yet another dependency in your project (but the API is mostly easier than rolling your own)

2. Use [Observables](https://rxjs.dev/)

    **Problem**: you've now introduced another level of complexity

3. Track the state of your component with `isMounted`

    **Problem:** it's an [anti-pattern][antipattern]

4. Create Your Own Cancellation Method

    **Problem:** it introduces another wrapper around Promises

5. Use [XMLHttpRequest][xmlhttprequest]

    **Problem:** The code is slightly more verbose than with `fetch`, but you can easily cancel a network request

Let's look at some of the suggestions:

### Keep Track of Mounted State

The following workaround gets recommended by popular React authors like [Robin Wieruch][rwieruch] or [Dan Abramov][overreacted].  
Those developers are surely much more smarter than I when it comes to React.  
They describe the solution as a **stopgap approach**. It's not perfect.

```javascript
function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    // we have to keep track if our component is mounted
    let isMounted = true

    const fetchData = () => {
      // set the state to "Loading" when we start the process
      setState(prevState => ({ ...prevState, isLoading: true }))

      // native browser-based Fetch API
      // fetch is promised-based
      fetch('https://ghibliapi.herokuapp.com/people')
        // we have to parse the response
        .then(response => response.json())
        // then we have to make sure that we only manipulate
        // the state if the component is mounted
        .then(jsonResponse => {
          if (isMounted) {
            setState(prevState => {
              return {
                ...prevState,
                isLoading: false,
                loadedData: [...jsonResponse],
              }
            })
          }
        })
        // catch takes care of the error state
        // but it only changes statte, if the component
        // is mounted
        .catch(_err => {
          if (isMounted) {
            setState(prevState => {
              return { ...prevState, isLoading: false, isError: true }
            })
          }
        })
    }

    // calling the function starts the process of sending ahd
    // storing the data fetching request
    fetchData()

    // the cleanup function toggles the variable where we keep track
    // if the component is mounted
    // note that this doesn't cancel the fetch request
    // it only hinders the app from setting state (see above)
    return () => {
      isMounted = false
    }
  }, [])

  return <JSX here />
}
```

(Here's a [CodeSandBox link][codesandboxpromise], if you're interested.)

Strictly speaking, you **don't cancel your data fetching request**. The workaround checks if the component is mounted. It avoids invoking `setState` if the component is not mounted.  
But the network request is still active.

### Create Your Own Cancellation Method

The above-mentioned [blog post][antipattern] introduces a wrapper around a Promise:

```javascript
const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise
```

```javascript
const makeCancelable = promise => {
  let hasCanceled_ = false

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
      error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
    )
  })

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true
    },
  }
}
```

Alternatively, you could introduce a cancellation method around [XMLHttpRequest][xmlhttprequest].  
Axios uses a similar approach [with a cancellation token](https://github.com/axios/axios#cancellation).

Here's the code from [StackOverflow][cancelpromise]:

```javascript
function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

// now you can setup the cancellation
var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();
```

Here's a [CodeSandBox example][codesandboxcancel].

Both solutions introduce a new helper function. The second one already points us into the direction of [XMLHttpRequest][xmlhttprequest].

### Low-Level API with XMLHttpRequest

The StackOverflow code wraps your API call into a Promise around [XMLHttpRequest][xmlhttprequest]. It also adds a cancellation token.

**Why not use XMLHttpRequest itself?**

Sure, it's not as readable as the browser-native `fetch`. But we've already established that we _must_ add extra code to cancel a promise.

XMLHttpRequest allows us to abort a request without using promises. Here's a simple implementation with `useEffect`.  
The `useEffect` function cleans up the request with [`abort`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort).

```javascript
function App() {
  const initialState = {
    isLoading: false,
    isError: false,
    loadedData: [],
  }

  const [state, setState] = React.useState(initialState)

  React.useEffect(() => {
    // we have to create an XMLHTTpRequest opject
    let request = new XMLHttpRequest()
    // we define the responseType
    // that makes it easier to parse the response later
    request.responseType = 'json'

    const fetchData = () => {
      // start the data fetching, set state to "Loading"
      setState(prevState => ({ ...prevState, isLoading: true }))

      // we register an event listener, which will fire off
      // when the data transfer is complete
      // we store the JSON response in our state
      request.addEventListener('load', () => {
        setState(prevState => ({
          ...prevState,
          isLoading: false,
          loadedData: [...request.response],
        }))
      })

      // we register an event listener if our request fails
      request.addEventListener('error', () => {
        setState(prevState => ({
          ...prevState,
          isLoading: false,
          isError: true,
        }))
      })

      // we set the request method, the url for the request
      request.open('GET', 'https://ghibliapi.herokuapp.com/people')
      // and send it off to the aether
      request.send()
    }

    // calling the fetchData function will start the data fetching process
    fetchData()

    // if the component is not mounted, we can cancel the request
    // in the cleanup function
    return () => {
      request.abort()
    }
  }, [])

  return <JSX here />
}
```

You can see it in action on [CodeSandBox][codesandboxxml].

That's not too bad, and you avoid the pesky React warning.  
The code is more difficult to understand because the XMLHttpRequest API is not very intuitive. Other than that, it's only some more lines than a promised-based `fetch` - but with cancellation!

## Conclusion

We've now seen a few approaches to **avoiding setting state on a unmounted component**.

The best approach is to trouble-shoot your code. Perhaps you can avoid unmounting your component.  
But if you need another method, you've now seen some ways to avoid a React warning when fetching data.

## Acknowledgments

The idea to use XMLHttpRequest is not mine.  
**[Cheng Lou](https://twitter.com/_chenglou)** introduced me to it in the [ReasonML Discord Channel](https://discord.gg/reasonml) and even gave an example in ReasonReact.

### Links

- [React: isMounted is an Antipattern][antipattern]
- [JavaScript for impatient programmers: Async Functions][asyncawait]
- [Promise - is it possible to force cancel a promise?][cancelpromise]
- [Prevent React setState on unmounted Component][preventonunmount]
- [A Complete Guide to useEffect][overreacted]
- [How to fetch data with React Hooks?][rwieruch]
- [What Color is Your Function?][color]
- [Promises, under the hood][promise]

[preventonunmount]: https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
[asyncawait]: https://exploringjs.com/impatient-js/ch_async-functions.html#await-working-with-promises
[cancelpromise]: https://stackoverflow.com/questions/30233302/promise-is-it-possible-to-force-cancel-a-promise
[bluebird]: https://github.com/petkaantonov/bluebird
[axios]: https://github.com/axios/axios
[antipattern]: https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
[xmlhttprequest]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
[rwieruch]: https://www.robinwieruch.de/react-hooks-fetch-data
[overreacted]: https://overreacted.io/a-complete-guide-to-useeffect/
[codesandboxpromise]: https://codesandbox.io/s/data-fetching-with-promise-n7qmx
[codesandboxxml]: https://codesandbox.io/s/data-fetching-with-xmlhttprequest-y5wi7
[codesandboxcancel]: https://codesandbox.io/s/data-fetching-with-cancellation-token-zb4tz
[color]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
[promise]: https://gist.github.com/gvergnaud/f354d70173daa828af49dc78dd4485ef