React Hooks - Can't Set State On An Unmounted Component
The Problem
How many times have you seen this? Dozens? Hundreds? Thousands? In my opinion
this is something that should be handled by the framework for how common one sees this,
but since that isn't possible the only way to handle this problem is to deal with it
in your application code. The most common reason we see this is because of long
computations where the state is set at the end, or REST calls that are waiting for
a response.
If you're using components the solve is pretty easy. All you need to do is have
a local component variable (not a state variable) that is set to true when constructed
and set it to false on unmount. Something like...
class Example extends Component {
let mounted = true
componentWillUnmount() {
this.mounted = false
}
const randomFunction = (bar) => {
if (this.mounted) {
this.setState({foo: bar})
}
}
render() {
//render code
}
}
The above will protect your setState from being called if the component is no longer mounted. You'll see code like this littered all over class components. With hooks, however we have some options.
State of the State
With functional components you need to think about things a bit differently. Functional
components only care about this render, so if there are multiple renders we need to
block we need to have something that can go across renders. Obviously we don't want
to use setState
since that is what we're trying to avoid, so we pull out the useRef
which can hold values across renders, but doesn't force a rerender like setState
.
Sounds perfect. This is what that looks like
const Example = () => {
const isMounted = useRef(true)
const [foo, setFoo] = useState()
useEffect(
() => () => {
isMounted.current = false
},
[]
)
const randomFunction = bar => {
if (isMounted.current) {
setFoo(bar)
}
}
return //render code
}
Using hooks we can extract this out of the component for reusability across our project.
const useIsMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
},
[]
)
return isMounted.current
}
const Example = () => {
const isMounted = useIsMounted()
const [foo, setFoo] = useState()
const randomFunction = bar => {
if (isMounted) {
setFoo(bar)
}
}
return //render code
}
Functional Wrapper - ifMounted
Sweet! Looks like a great solve, but we still have a problem where we have to
wrap almost every setState
in an if statement if we want to really protect
ourselves going forward, littering our otherwise clean code.
What if we could reduce this to a single function call that checks whether the
component is mounted before calling setState
const useIfMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
},[]
)
const ifMounted = useCallback(
func => {
if (isMounted.current && func) {
func()
}
},[]
)
return ifMounted
}
const Example = () => {
const ifMounted = useIfMounted()
const [foo, setFoo] = useState()
const randomFunction = bar => {
ifMounted(() => setFoo(bar))
}
return //render code
}
Now I can safely wrap any setState
call in a single line with the ifMounted
hook and never have to worry about setting state on an unmounted component
ever again.
Done!
That's it! Have a better solution or have feedback? Leave a comment below!