question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Dependency-based reinitialization of useState

See original GitHub issue

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

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:16
  • Comments:18 (2 by maintainers)

github_iconTop GitHub Comments

4reactions
peterjurascommented, Mar 22, 2020

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

2reactions
peterjurascommented, Feb 1, 2020

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

A React hook to handle state with dependencies
A description of a custom React hook that can simplify dealing with state that must be initialised and potentially invalidated based on one ......
Read more >
React useState hook with dependency - Stack Overflow
Where I have an outer component that provides some data and an inner component that lets the user edit it (by modifying a...
Read more >
Hooks API Reference - React
They let you use state and other React features without writing a class. This page describes the APIs for the built-in Hooks in...
Read more >
Hooks and state 102: the Dependency array in useEffect()
To declare an effect in your component, you use the useEffect React hook, and call it at the top level of your component....
Read more >
React Hooks cheat sheet: Best practices with examples
useState lets you use local state within a function component. You pass the initial state to this function and it returns a variable...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found