2023
This is typically happening when running setState after waiting for an asynchronous function. If the component is no longer mounted when the response arrives, attempting to set state when the response arrives will result in the error message you are seeing.
useEffect(
() => {
someAsyncFunction().then(data => {
setData(data) // ❌ unsafe call of setData
})
},
…
)
We can fix this by setting a local flag in the effect, and using the effect clean-up function to switch the flag when the component unmounts. The Fetching Data guide from the React docs has more details -
useEffect(
() => {
let mounted = true // ✅ component is mounted
someAsyncFunction().then(data => {
if (mounted) setData(data) // ✅ setState only if mounted
})
return () => {
mounted = false // ✅ component is unmounted
}
},
…
)
custom hook, no callbacks
But can you imagine writing all of that each time you needed to run an asynchronous function in one of your components? Thankfully we can easily encapsulate this logic in a custom hook.
Other answers here provide a clumsy API that ask the user to specify onSuccess or onError callbacks. This reminds me of the code everyone was writing before we had promises. Let's see if we can do better -
import { DependencyList, useEffect, useState } from "react"
type UseAsyncHookState<T> =
| { kind: "loading" }
| { kind: "error", error: Error }
| { kind: "result", data: T }
function useAsync<T>(func:() => Promise<T>, deps: DependencyList) {
const [state, setState] = useState<UseAsyncHookState<T>>({ kind: "loading" })
useEffect(
() => {
let mounted = true
func()
.then(data => mounted && setState({ kind: "result", data }))
.catch(error => mounted && setState({ kind: "error", error }))
return () => {
mounted = false
}
},
deps,
)
return state
}
Our hook helps us detangle complex API logic from component state and lifecycle. With a well-defined type and simple API method -
type User = { … }
async function getUser(id: number): Promise<User> {
return …
}
We can write our component in declarative way, without the need for callbacks -
function UserProfile(props: { userId: number }) {
const user = useAsync( // ✅ type automatically inferred
() => getUser(props.userId), // async call
[props.userId], // dependencies
)
switch (user.kind) { // ✅ exhaustive switch..case
case "loading":
return <Loading />
case "result":
return <UserData user={user.data} />
case "error":
return <Error message={user.error.message} />
}
}
This also takes advantage of TypeScript 5's new switch..case exhaustive completions, ensuring that our component correctly checks for all state possibilities of the hook.
vanilla javascript
For non-TypeScript users, here's the vanilla hook that does the exact same thing -
import { useState, useEffect } from "react"
function useAsync(func, deps) {
const [state, setState] = useState({ kind: "loading" })
useEffect(
() => {
let mounted = true
func()
.then(data => mounted && setState({ kind: "result", data }))
.catch(error => mounted && setState({ kind: "error", error }))
return () => {
mounted = false;
}
},
deps
)
return state
}