react: useState not bailing out when state does not change

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

As demonstrated in this codesandbox, trying to implement a pattern similar to the one discussed in https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops results in an infinite loop, even if the value of the state does not change. This seems like a bug, because, as documented here, If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn’t have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

What is the expected behavior?

Since the state does not change, bail out on the re-render. This can be worked around by adding a check before setState to check if the state has changed before calling the function.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 17
  • Comments: 41 (5 by maintainers)

Most upvoted comments

@acutmore Calling setState directly from a component render function is a footgun and should probably be an explicit error because sometimes it works and sometimes it doesn’t. Your state change needs to be extracted to an effect hook.

function Component(props) {
  const [state, setState] = useState(0);
  const condition = someCondition(props);
  useEffect(() => {
    if (condition) {
      setState(0);  // This is fine
    }
  }, [ condition ]);
  return <div />;
}

React does not offer a strong guarantee about when it invokes render functions. Render functions are assumed to be pure and there should be absolutely no difference for correctness regardless of how many times they’re called. If calling it an extra time causes a bug, it’s a problem with the code that needs to be fixed.

From the performance point of view, the cost of re-running a single function is usually negligible. If it’s not, you should add some useMemos to the parts that are expensive. React does guarantee that if a bailout happens, it will not go updating child components. Even though it might in some cases re-run the component function itself.

I don’t think there is something actionable here, so I think we can close this.

I think at the time we decided that we don’t actually know if it’s safe to bail out in all cases until we try render again. The “bailout” here means that it doesn’t go ahead to render children. But re-running the same function might be necessary (for example, if a reducer is inline and we don’t know if we bail out until we re-run the reducer on next render). So for consistency we always re-run it on updates during the render phase.

I don’t think I agree with you the pattern in https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops is broken. It’s different from your code sample which doesn’t track prevState.

About preventing re-renders when state does not change. I use a custom hook like this:

function areStrictEqual(a, b) {
  return a === b;
}

function useStateUpdate(initialState, areEqual = areStrictEqual) {
  const [state, setState] = React.useState(initialState);
  const stateRef = React.useRef(state);
  const areEqualRef = React.useRef(areEqual);
  areEqualRef.current = areEqual;

  const updateState = React.useCallback(stateOrReducer => {
    const nextState =
      typeof stateOrReducer === "function"
        ? stateOrReducer(stateRef.current)
        : stateOrReducer;

    if (!areEqualRef.current(stateRef.current, nextState)) {
      stateRef.current = nextState;
      setState(nextState);
    }
  }, []);

  return [state, updateState];
}

Example usage: https://codesandbox.io/s/misty-frost-yiogh

I think the main issue here is that the usePrevious hook given in docs didn’t take render phase updates into account. It doesn’t update the previous value until after render phase. But in this case, we need to read the latest previous prop immediately after an update is scheduled during render phase.

stop worrying about render counts until you identified a performance bottleneck

For me it’s not performance but infinite renders. It would be handy if I didn’t have to worry about checking the current state before resetting it to something.

function Component(props) {
  const [state, setState] = useState(0);
  if (someCondition(props)) {
    setState(0);  // Error! Too Many ReRenders
  }
  return <div />;
}

Can fix here without issue, but in a more complex component, with custom hooks passing state around, this is slightly harder to achieve.