Updating state to the same state directly in the component body

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.

Similarly you could use the useEffect hook and completely remove the dependency array so it’s an effect that would/could fire each and every render cycle.

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

  useEffect(() => {
    setState(1);
  });

  return <div>Component</div>;
}

Again, the useEffect hook callback is guaranteed to be invoked at least once, enqueueing a state update. React will “see” 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.

Leave a Comment