Created
August 6, 2021 16:24
-
-
Save nfarina/b733842e1541008634541b0503c882b4 to your computer and use it in GitHub Desktop.
A version of React's `useState` that resets the value to initial whenever the given dependency array changes. Very helpful when you need to reset some internal state as the result of getting new props.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { DependencyList, Dispatch, SetStateAction, useState } from "react"; | |
/** | |
* This is like useState() but with the added feature of returning the initial | |
* value whenever the dependency list changes. This is super useful for allowing | |
* components to "reset" some internal state as a result of getting new props. | |
*/ | |
export function useResettableState<S>( | |
initial: S | (() => S), | |
deps: DependencyList, | |
): [S, Dispatch<SetStateAction<S>>] { | |
const [innerValue, setInnerValue] = useState(initial); | |
const [prevDeps, setPrevDeps] = useState(deps); | |
// If the deps changed, reset our state to initial. | |
// Calling setState during render is rare but supported! | |
// https://github.com/facebook/react/issues/14738#issuecomment-461868904 | |
if (depsChanged(deps, prevDeps)) { | |
setPrevDeps(deps); | |
setInnerValue(initial); | |
} | |
return [innerValue, setInnerValue]; | |
} | |
function depsChanged( | |
previous: DependencyList | undefined, | |
current: DependencyList | undefined, | |
): boolean { | |
if (previous === undefined && current === undefined) return false; | |
if (previous === undefined || current === undefined) return true; | |
if (previous.length !== current.length) { | |
console.error( | |
"useResettableState(): Dependency array size changed between renders!", | |
); | |
return false; | |
} | |
// Lengths are the same; must compare values. | |
for (let i = 0; i < previous.length; i += 1) { | |
if (previous[i] !== current[i]) { | |
return true; | |
} | |
} | |
// Unchanged! | |
return false; | |
} |
Ok yes after testing a bit to verify: when your state “resets” as the result of a dep of useResettableState changing, your component will be rendered twice, but any effects will only be triggered once (after the 2nd render where the state was reset).
So this hook can help avoid “blips” in the DOM as the result of effect hooks firing twice in a row (once with stale not-reset-yet data).
For your information, this is what I finally came to, a twisted version of yours:
export function useResettableState<S>(
initial: S | (() => S),
deps: DependencyList,
): [S, Dispatch<SetStateAction<S>>] {
let [innerValue, setInnerValue] = useState(initial);
const [prevDeps, setPrevDeps] = useState(deps);
if (depsChanged(deps, prevDeps)) {
setPrevDeps(deps);
setInnerValue(initial);
innerValue = initial;
}
return [innerValue, setInnerValue];
}
This way, immediately it resets, not at the 2nd render as I was mentioning.
Yes I remember considering this at first as well but there was some pitfall (that I should have documented) - if you encounter it please let me know so I can note it here!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Glad you found it helpful! I believe this method is superior to useEffect, as I am pretty sure the render method is simply called twice in a row before React “moves on” and propagates state to the caller. But - I’ll do a little testing later to make sure.