Search code examples
javascriptreactjsreact-hooksinfinite-loop

Updating state to the same value directly in the component body during render causes infinite loop


Let's say I have this simple dummy component:

const Component = () => {

  const [state, setState] = useState(1);

  setState(1);

  return <div>Component</div>
}

In this code, I update the state to the same value as before directly in the component body. But, this causes too many re-renders even if the value stayed the same.

And as I know, in React.useState, if a state value was updated to the same value as before - React won't re-render the component. So why is it happening here?

However, if I try to do something similar with useEffect and not directly in the component body:

const Component = () => {

  const [state, setState] = useState(1);

  useEffect(() => {
    setState(1);
  }, [state])

  return <div>Component</div>
}

This is not causing any infinite loop and goes exactly according to the rule that React won't re-render the component if the state stayed the same.

So my question is: Why is it causing an infinite loop when I do it directly in the component body and in the useEffect it doesn't?

Does anyone have some "behind the scenes" explanation for this?


Solution

  • TL;DR

    The first example is an unintentional side-effect and will trigger rerenders unconditionally while the second is an intentional side-effect and allows the React component lifecycle to function as expected.

    Answer

    I think you are conflating the "Render phase" of the component lifecycle when React invokes the component's render method to compute the diff for the next render cycle with what we commonly refer to as the "render cycle" during the "Commit phase" when React has updated the DOM.

    See the component lifecycle diagram:

    enter image description here

    Note that in React function components that the entire function body is the "render" method, the function's return value is what we want flushed, or committed, to the DOM. As we all should know by now, the "render" method of a React component is to be considered a pure function without side-effects. In other words, the rendered result is a pure function of state and props.

    In the first example the enqueued state update is an unintentional side-effect that is invoked outside the normal component lifecycle (i.e. mount, update, unmount).

    const Component = () => {
      const [state, setState] = useState(1);
    
      setState(1); // <-- unintentional side-effect
    
      return <div>Component</div>;
    };
    

    It's triggering a rerender during the "Render phase". The React component never got a chance to complete a render cycle so there's nothing to "diff" against or bail out of, thus the render loop occurs.

    The other example the enqueued state update is an intentional side-effect. The useEffect hook runs at the end of the render cycle after the next UI change is flushed, or committed, to the DOM.

    const Component = () => {
      const [state, setState] = useState(1);
    
      useEffect(() => {
        setState(1); // <-- intentional side-effect
      }, [state]);
    
      return <div>Component</div>;
    }
    

    The useEffect hook is roughly the function component equivalent to the class component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods. It is guaranteed to run at least once when the component mounts regardless of dependencies. The effect will run once and enqueue a state update. React will "see" that the enqueued value is the same as the current state value and won't trigger a rerender.

    The takeaway here is to not code unintentional and unexpected side-effects into your React components as this results in and/or leads to buggy code.