Raise your hand ✋, if you've seen this error in your React application:
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:
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, callingsetState()
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
.
Example: Call to an API with fetch
(Promise-based)
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. And that's why you either need event listeners, callbacks, promises, or async/await.
The problem is that you can't cancel a Promise.
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
-
Use a third-party library like bluebird or axios.
Problem: yet another dependency in your project (but the API is mostly easier than rolling your own)
-
Use Observables
Problem: you've now introduced another level of complexity
-
Track the state of your component with
isMounted
Problem: it's an anti-pattern
-
Create Your Own Cancellation Method
Problem: it introduces another wrapper around Promises
-
Use 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 or Dan Abramov.
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.
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, 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 introduces a wrapper around a Promise:
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
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.
Axios uses a similar approach with a cancellation token.
Here's the code from StackOverflow:
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.
Both solutions introduce a new helper function. The second one already points us into the direction of XMLHttpRequest.
Low-Level API with XMLHttpRequest
The StackOverflow code wraps your API call into a Promise around 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
.
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.
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 introduced me to it in the ReasonML Discord Channel and even gave an example in ReasonReact.
Links
- React: isMounted is an Antipattern
- JavaScript for impatient programmers: Async Functions
- Promise - is it possible to force cancel a promise?
- Prevent React setState on unmounted Component
- A Complete Guide to useEffect
- How to fetch data with React Hooks?
- What Color is Your Function?
- Promises, under the hood
Top comments (2)
I recently also found this method to cancel a fetch request: developers.google.com/web/updates/...
Looks like it's fairly widely supported now.
Wow, that's great to know. Thank you for pointing this out.