Dependency-based reinitialization of useState
See original GitHub issueDo you want to request a feature or report a bug?
Feature
What is the current behavior?
Consider I want to have a dynamically calculated list of options, and a piece of state that represents the currently selected option. I can achieve this using hooks as follows:
const options = useMemo(() => {
// Calculate the options
}, [dep1, dep2]);
const [choice, setChoice] = useState(options[0]);
const result = useMemo(() => {
// Calculate the displayed result
}, [choice]);
However, a problem occurs if either dep1
or dep2
changes. The list of options changes, which means choice
may no longer be valid. To fix this, I must split choice
into a selected value and a memoized value that checks for validity:
const [selectedChoice, setSelectedChoice] = useState(options[0]);
const choice = useMemo(() => {
if (options.includes(selectedChoice) {
return selectedChoice;
} else {
setSelectedChoice(options[0]);
return options[0];
}
}, [options, selectedChoice]);
What is the expected behavior?
It would useful if we could declare dependencies for useState
, in the same way that we can for useMemo
, and have the state reset back to the initial state if they change:
const [choice, setChoice] = useState(options[0], [options]);
In order to allow preserving the current value if its valid, React could supply prevState
to the initial state factory function, if any exists, e.g.
const [choice, setChoice] = useState(prevState => {
if (prevState && options.includes(prevState) {
return prevState;
else {
return options[0];
}
}, [options]);
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.8.0-alpha.1
Issue Analytics
- State:
- Created 5 years ago
- Reactions:16
- Comments:18 (2 by maintainers)
For anyone that runs into this issue, I published a custom hook called
useStateWithDeps
as an npm package with my implementation proposal from my comment above.NPM:
use-state-with-deps
@gaearon I understand that this pattern is discouraged, but I think it becomes vital if you want to write hooks and components and that react to updates while being mounted in the correct way. All hooks provide a smooth way to react to updates after being mounted - except
useState
- since it does not allow for a declarative state reset when it would be needed. While it is possible to work around this issue by setting the state again, it adds more complexity in the code and needs additional render method calls (and while additional render calls shouldn’t matter too much in terms of performance, why have them if they can be avoided?)Current issues with the
getDerivedStateFromProps
migration documentationFurthermore, the example of the documentation has some issues, because the hook is returning the wrong value. Let’s look at the example:
Here,
isScrollingDown
is returned which was based onprevRow
, although the correct value would beprevRow !== null && row > prevRow
. While react will re-render before continuing, the current render method will continue, because the execution is synchronous. This is especially problematic when using hooks and expecting the result to be consistent with its input.Let’s look at a component where transferring the example from the documentation 1 to 1 would lead to issues:
In this example, an exception is thrown when the type changes, since the returned value by the hook is based on a previous prop. This could be fixed by making the state variable re-assignable:
But it still feels like this adds a lot of complexity to the code, I’m currently even using refs instead of state in a library hook that is used multiple 100 times to ensure that the returned values are consistent and the hook is not responsible for render aborts / re-calls.
How a resettable
useState
could helpLet’s assume that
useState
has a dependency array argument, similar to other hooks and rewrite ouruseAnimation
hook:I see three immediate benefits here:
Conclusion
I really think that a dependency array for
useState
could add a lot of value. What were the reasons why this hook was the only one that came without it?