React: Dependency-based reinitialization of useState

Created on 31 Jan 2019  路  14Comments  路  Source: facebook/react

Do 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

Discussion

Most helpful comment

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

Furthermore, the example of the documentation has some issues, because the hook is returning the wrong value. Let's look at the example:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

Here, isScrollingDown is returned which was based on prevRow, although the correct value would be prevRow !== 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:

function getAnimationFromType(type) {
  switch (type) {
    case "Scale":
      return { scale: { x: 0, y: 0 } };
    case "Rotate":
      return { rotate: { deg: 0 } };
    default:
      throw new Error("Invalid Type");
  }
}

function useAnimation(type) {
  const [animation, setAnimation] = useState(getAnimationFromType(type));
  const [prevType, setPrevType] = useState(type);

  if (prevType !== type) {
    setAnimation(getAnimationFromType(type));
    setPrevType(type);
  }

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation; // Warning! This returns an object with properties that don't match the type!
}

function MyComponent({ type }) {
  const animation = useAnimation(type);

  // Let's assume we want to work with a value that has been returned
  // from the hook in the render function. We might receive an Exception, since
  // the returned value from the useAnimation hook might not be in-sync
  // with our type prop.
  let valueFromAnimationHook;
  switch (type) {
    case "Scale":
      // ERROR: This will throw if the type changed, since animation is still based
      // on "Rotate"
      valueFromAnimationHook = animation.scale.x + animation.scale.y;
      break;
    case "Rotate":
      // ERROR: This will throw if the type changed, since animation is still based
      // on "Scale"
      valueFromAnimationHook = animation.rotate.deg;
      break;
    default:
      break;
  }

  return <OtherComponent animation={animation} />;
}

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:

function useAnimation(type) {
  let [animation, setAnimation] = useState(getAnimationFromType(type));
  const [prevType, setPrevType] = useState(type);

  if (prevType !== type) {
    const newAnimation = getAnimationFromType(type);
    setAnimation(newAnimation);
    animation = newAnimation;

    setPrevType(type);
  }

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation;
}

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 help

Let's assume that useState has a dependency array argument, similar to other hooks and rewrite our useAnimation hook:

function useAnimation(type) {
  // type is passed as a dependency, if type changes, the current state should be
  // discarded and replaced with the first argument which has been provided as the "initial value".
  // If the type did not change, the state remains untouched and represents the last
  // value that was passed with setAnimation
  const [animation, setAnimation] = useState(getAnimationFromType(type), [type]);

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation;
}

I see three immediate benefits here:

  • The code is shorter and can focus on what it should be doing -> Animating, not working around react patterns
  • We don't need to keep track of the previous type in another state field
  • There is no more potential to return a stale value, because the state is in sync with our received props.

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?

All 14 comments

The idiomatic way to reset state based on props is here:

https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

In other words:

const [selectedChoice, setSelectedChoice] = useState(options[0]);
const [prevOptions, setPrevOptions] = useState(options);

if (options !== prevOptions) {
  setPrevOptions(options);
  setSelectedChoice(options[0]);
}

I don't think we want to encourage this pattern commonly so we're avoiding adding a shorter way (although we considered your suggestion).

I know this is closed but my question is very similar to this. Suppose we want to implement something like getDerivedStateFromProps, but instead of tracking for previousWhatever and manually compare it, use useEffect with that dependency. Would that roughly equivalent for React? I'm not sure the exact rendering order or how this would affect the rendering, because currently in my application, I reset the state when props change in useEffect

For example,

const [selectedChoice, setSelectedChoice] = useState(options[0]);

useEffect(() => {
  setSelectedChoice(options[0]);
}, [options])

Would this work the same as your suggestion above? Is this another idiomatic way of handling getDerivedStateFromProps? Thanks

Hi :wave:

I've come here with exactly same thinking as @WiNloSt.

I'm building a complex hook that uses state internally. And I want to completely reinitialize it when an incoming parameter is changing. A natural way of doing this would be to provide [deps] array into useState() hook. But there is no such thing :confused:

With the useEffect() approach suggested above I'm afraid there will be unwanted side-effects that may use remembered state values with the new incoming parameter (as we know setState() is asynchronous). So having dependencies list in useState() would really help here.

Thanks.

@e1himself I've been setting state explicitly since the last time I posted here with no issue. To give you more context I'm developing a SaaS app with active users. So I don't think there's gonna be an issue with this approach if you write hook idiomatically (don't lie about the dependencies).

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

Furthermore, the example of the documentation has some issues, because the hook is returning the wrong value. Let's look at the example:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

Here, isScrollingDown is returned which was based on prevRow, although the correct value would be prevRow !== 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:

function getAnimationFromType(type) {
  switch (type) {
    case "Scale":
      return { scale: { x: 0, y: 0 } };
    case "Rotate":
      return { rotate: { deg: 0 } };
    default:
      throw new Error("Invalid Type");
  }
}

function useAnimation(type) {
  const [animation, setAnimation] = useState(getAnimationFromType(type));
  const [prevType, setPrevType] = useState(type);

  if (prevType !== type) {
    setAnimation(getAnimationFromType(type));
    setPrevType(type);
  }

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation; // Warning! This returns an object with properties that don't match the type!
}

function MyComponent({ type }) {
  const animation = useAnimation(type);

  // Let's assume we want to work with a value that has been returned
  // from the hook in the render function. We might receive an Exception, since
  // the returned value from the useAnimation hook might not be in-sync
  // with our type prop.
  let valueFromAnimationHook;
  switch (type) {
    case "Scale":
      // ERROR: This will throw if the type changed, since animation is still based
      // on "Rotate"
      valueFromAnimationHook = animation.scale.x + animation.scale.y;
      break;
    case "Rotate":
      // ERROR: This will throw if the type changed, since animation is still based
      // on "Scale"
      valueFromAnimationHook = animation.rotate.deg;
      break;
    default:
      break;
  }

  return <OtherComponent animation={animation} />;
}

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:

function useAnimation(type) {
  let [animation, setAnimation] = useState(getAnimationFromType(type));
  const [prevType, setPrevType] = useState(type);

  if (prevType !== type) {
    const newAnimation = getAnimationFromType(type);
    setAnimation(newAnimation);
    animation = newAnimation;

    setPrevType(type);
  }

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation;
}

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 help

Let's assume that useState has a dependency array argument, similar to other hooks and rewrite our useAnimation hook:

function useAnimation(type) {
  // type is passed as a dependency, if type changes, the current state should be
  // discarded and replaced with the first argument which has been provided as the "initial value".
  // If the type did not change, the state remains untouched and represents the last
  // value that was passed with setAnimation
  const [animation, setAnimation] = useState(getAnimationFromType(type), [type]);

  useEffect(() => {
    // TODO: Animate
  }, [animation]);

 return animation;
}

I see three immediate benefits here:

  • The code is shorter and can focus on what it should be doing -> Animating, not working around react patterns
  • We don't need to keep track of the previous type in another state field
  • There is no more potential to return a stale value, because the state is in sync with our received props.

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?

@peterjuras I just realized that it might be possible to achieve similar dependency-based re-initialization effect by using key property mechanics.

<YourStatefulComponent key={id} />

The component will unmount and re-mount every time key property changes.

Though it's a bit hacky and certainly is s less expressive as useState(..., [id]) would be. But at least you'll be able to easily work-around stale state bug described above.

Stumbled on same issue couple of days ago, I am quite new to React and maybe I had solved this the wrong way, but here is my solution:

import { useState } from 'react'
import { shallowEqualArrays } from "shallow-equal";

function useStateWithDependency(initialValue, dependencyNew) {
    const [{state, dependency}, setState] = useState({state: initialValue, dependencyNew});

    return [(shallowEqualArrays(dependency, dependencyNew)) ? state : initialValue
            , 
            (val) => {
                setState({state: val, dependency: dependencyNew});
            }];
}

export { useStateWithDependency }

Take this simple example

So I would expect the buttonStatus to change when you fulfill both conditions, but it doesn't happen. I assume this falls under the ops usecase, correct ?

function MyComponent({ type }) {
  const [acceptedTerms, setTerms] = useState(false);
  const [acceptedDataConsent, setConsent] = useState(false);

  const [buttonStatus, setButtonStatus] = useState(
    acceptedTerms && acceptedDataConsent
  );

  return (
    <>
      <Checkbox
        type="checkbox"
        checked={acceptedTerms}
        onChange={() => {
          setTerms(!acceptedTerms);
        }}
      />
      <Checkbox
        type="checkbox"
        checked={acceptedDataConsent}
        onChange={() => {
          setConsent(!acceptedDataConsent);
          debugger;
        }}
      />
      <Button
        style={{ margin: "24px 0" }}
        disabled={!buttonStatus}
        onClick={e => {
          setModalState(true);
        }}
      >
        <Text6>{buttonText}</Text6>
      </Button>
    </>
  );
}

Take this simple example

So I would expect the buttonStatus to change when you fulfill both conditions, but it doesn't happen. I assume this falls under the ops usecase, correct ?

function MyComponent({ type }) {
  const [acceptedTerms, setTerms] = useState(false);
  const [acceptedDataConsent, setConsent] = useState(false);

  const [buttonStatus, setButtonStatus] = useState(
    acceptedTerms && acceptedDataConsent
  );

  return (
    <>
      <Checkbox
        type="checkbox"
        checked={acceptedTerms}
        onChange={() => {
          setTerms(!acceptedTerms);
        }}
      />
      <Checkbox
        type="checkbox"
        checked={acceptedDataConsent}
        onChange={() => {
          setConsent(!acceptedDataConsent);
          debugger;
        }}
      />
      <Button
        style={{ margin: "24px 0" }}
        disabled={!buttonStatus}
        onClick={e => {
          setModalState(true);
        }}
      >
        <Text6>{buttonText}</Text6>
      </Button>
    </>
  );
}

Not sure this falls under the same use case, from your code example, wouldn't it suffice to directly calculate the buttonStatus without useState? E.g.:

function MyComponent({ type }) {
  const [acceptedTerms, setTerms] = useState(false);
  const [acceptedDataConsent, setConsent] = useState(false);

  const buttonStatus = acceptedTerms && acceptedDataConsent;

  return (
    // ...

Not sure this falls under the same use case, from your code example, wouldn't it suffice to directly calculate the buttonStatus without useState? E.g.:

function MyComponent({ type }) {
  const [acceptedTerms, setTerms] = useState(false);
  const [acceptedDataConsent, setConsent] = useState(false);

  const buttonStatus = acceptedTerms && acceptedDataConsent;

  return (
    // ...

indeed that's how I approached it in the end. But I assumed it would work just the same with useState. But oddly it didn't

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

I've needed this a few times. I've made my own workaround helper hook that uses useMemo() (since it does have a deps array).

Regarding the workaround in the Hooks FAQ, there's something I dislike conceptually about the current component render call completing with stale data, then invalidating itself with a setState() to trigger another render. It seems so conceptually scrappy that even though my workaround is less pure I still prefer it.

Agree with @dhoulb. Rendering stale data and then rerunning the renderer again although results with acceptable user-facing outcome but is not only inefficient but is technically incorrect.
"We do not encourage this", "you probably don't need this" is not technical enough. You see people face problems, need the feature and the suggested workarounds are inefficient and technically buggy if the renderer uses current prop values along with the state derived off previous props.

Could you please be more technical and describe technical problems to adding dependencies to useState?

Was this page helpful?
0 / 5 - 0 ratings