Reputation: 1102
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?
Upvotes: 14
Views: 4597
Reputation: 354
As support for Drew Reese's answer, here's a little playground that lets you see for yourself how React behaves when setState is directly in the component body versus inside a useEffect or conditional where it is only triggered after the first render completes.
As Drew Reese says, if setState(), even to the same primitive value, happens during the execution of the component body, React will get stuck in an infinite loop. Somehow it's not diffing anything during that first render, so it doesn't matter that setState() is set to the same value that it was initialized with.
Here is a gold mine of wisdom for this situation: https://github.com/facebook/react/issues/20817#issuecomment-778672150
Use the JSFiddle to be able to edit / commment / uncomment: https://jsfiddle.net/7b84xm9u/
const {
useState,
useEffect,
useRef
} = React;
const {createRoot} = ReactDOM
const Component = () => {
const renderCountRef = useRef(0);
const [same, setSame] = useState('same');
const [count, setCount] = useState(0);
renderCountRef.current++;
console.log(renderCountRef.current)
document.getElementById('render-count').innerHTML = renderCountRef.current
// Doesn't cause any rerenders
useEffect(() => {
setSame('same');
});
// 👇 Will cause an infinite rerender loop. (Uncomment to see the error, in dev console)
// /* <= uncomment */ setSame('same');
// Use the debugger to see that nothing ever actually has a chance to get committed to the DOM (the React area remains blank)
// when setState is called in the body of the component
// /* <= uncomment */ debugger
// A setState that happens while the component body is being executed
// will cause another execution of the component body.
// Hence the below will cause an infinite rerender, after first render.
if (renderCountRef.current > 1) {
setSame('same');
}
return (
<div>
<h2> React </h2>
I'm not erroring! 😁 <br />
I've rendered {renderCountRef.current} times. <br />{" "}
<br />
{/** Doesn't cause a rerender **/}{" "}
<button
onClick={() => {
setSame("same")
}}
>
Set state to same primitive value{" "}
</button>{" "}
<br /> <br />
<button onClick={() => {setCount((prev) => prev + 1)}}>Click me to crash! Count: {count}</button>
</div>
)
};
createRoot(document.getElementById('root')).render(<Component />)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div style="display: flex; font-family: sans-serif; gap: 20px; padding-bottom: 20px;">
<div style="border: solid 1px blue;" id="root"></div>
<div style="border: 1px dashed green">
<h2>Outside React</h2>
Render count: <span id="render-count">0</span>
</div>
Upvotes: 0
Reputation: 933
I think the simplest way to explain this is...
In your first example setState
is called every time the component re-renders, and will itself cause the component to re-render, hence the infinite loop.
I should note here that a component re-rendering does not necessarily mean there will be updates to the DOM, thanks to React's virtual DOM.
In your second example the useEffect
which triggers the setState
call is only called when its dependencies (second argument) change, and since its only dependency is the state
it will not be triggered again if the state has not changed.
You should never change any state during the render of a component. These can be handled in effects, which basically exist to prevent your infinite loop scenario.
Upvotes: 0
Reputation: 202605
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.
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:
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.
Upvotes: 12
Reputation: 169
When invoking setState(1)
you also trigger a re-render since that is inherently how hooks work. Here's a great explanation of the underlying mechanics:
How does React.useState triggers re-render?
Upvotes: -1