Skip to content

Instantly share code, notes, and snippets.

@AlpacaGoesCrazy
Last active December 7, 2020 17:49
Show Gist options
  • Save AlpacaGoesCrazy/25e3a15fcd4e57fb8ccd408d488554d7 to your computer and use it in GitHub Desktop.
Save AlpacaGoesCrazy/25e3a15fcd4e57fb8ccd408d488554d7 to your computer and use it in GitHub Desktop.
Hook for react state which prevents updates on unmounted components
import { useEffect, useRef, useState } from 'react'
/*
If you attempt to set some state after asynchronous request it may happen that component you wish to set state on has been unmounted.
This will trigger "Warning: Can’t call setState (or forceUpdate) on an unmounted component." warning.
This hooks is `useState` hook which prevents setting state on an unmounted component
Usage:
const [myState, mySafeSetState] = useSafeState(initialValue)
*/
const useSafeState = (initialValue) => {
const _isMounted = useRef() // useRef to memorize if the component is mounted between renders
const [state, setState] = useState(initialValue)
useEffect(() => {
_isMounted.current = true
return () => {
_isMounted.current = false
}
})
const safeSetState = (...args) => {
if(_isMounted.current) { // do not call setState if the component already unmounted
setState(...args)
}
}
return [state, safeSetState]
}
export default useSafeState
@AlpacaGoesCrazy
Copy link
Author

@AlpacaGoesCrazy just a thought-wouldn't it be better to solve this one level higher?
So use https://github.com/mauricedb/use-abortable-fetch instead of like a regular useFetch and you should have it covered without hacking around the useState. Right?

It is not always the case that your async function is invoked in the same component where it was declared, and not all your async functions are API calls. And if you do decide to use that library you will need to track component unmounting and manually call abort.

On top of that you might not want to abort your async request. If you start the request and navigate away you still might want to finish it and put it in cache so there is no need to invoke it on navigation back. The point is to not update state of the unmounted component when you finish your request like setIsLoading(false)

@troygoode
Copy link

Thanks for creating this @AlpacaGoesCrazy! Super useful. I've modified it a bit to work in TypeScript version – type-compatible drop-in replacement for useState:

https://gist.github.com/troygoode/0702ebabcf3875793feffe9b65da651a

@alexbepple
Copy link

alexbepple commented Dec 7, 2020

I have found that this does not preserve setState function identity (in contrast to React), which led to suprising bugs when replacing useState with useSafeState. I have therefore modified the implementation to:

export const useSafeState = (initialValue) => {
    const _isMounted = useRef();
    const [state, setState] = useState(initialValue);
    const _setState = useRef((...args) => {
        if (_isMounted.current) { setState(...args); }
    });

    useEffect(() => {
        _isMounted.current = true;
        return () => { _isMounted.current = false; };
    });
    return [state, _setState.current];
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment