Skip to content

Instantly share code, notes, and snippets.

@AlpacaGoesCrazy
Last active December 7, 2020 17:49
Show Gist options
  • Select an option

  • Save AlpacaGoesCrazy/25e3a15fcd4e57fb8ccd408d488554d7 to your computer and use it in GitHub Desktop.

Select an option

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
@capaj

capaj commented Jun 14, 2019

Copy link
Copy Markdown

@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?

@AlpacaGoesCrazy

Copy link
Copy Markdown
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
Copy Markdown

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

alexbepple commented Dec 7, 2020

Copy link
Copy Markdown

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