Skip to content

Instantly share code, notes, and snippets.

@nfarina
Created August 6, 2021 16:24
Show Gist options
  • Save nfarina/b733842e1541008634541b0503c882b4 to your computer and use it in GitHub Desktop.
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.
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;
}
@lcswillems
Copy link

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?

@nfarina
Copy link
Author

nfarina commented Sep 9, 2024

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.

@nfarina
Copy link
Author

nfarina commented Sep 9, 2024

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).

@lcswillems
Copy link

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.

@nfarina
Copy link
Author

nfarina commented Sep 9, 2024

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