-
-
Save nfarina/b733842e1541008634541b0503c882b4 to your computer and use it in GitHub Desktop.
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; | |
} |
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.
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!
Hey @nfarina ! Thanks for sharing this! If I'm not wrong, in case the deps change, the state will only be changed at the 2nd render?
So it is just equivalent to set the state in a
useEffect
?