6

I have the following code (CodeSandbox):

function App() {
  const [blah, setBlah] = useState(true);
  console.log('BLAH', blah);
  setBlah(true);

  return <button onClick={() => setBlah(true)}>Blah</button>;
}

I understand that setBlah(true) at the top level of the component is causing too many re-renders. What I don't understand is if you comment out the top level setBlah(true) and start mashing the "Blah" button, why does the component not re-render? Both are setting the state of blah to true over and over, yet only the top level setBlah(true) causes a re-render.

From the React docs on Bailing out of a state update: "Note that React may still need to render that specific component again before bailing out." The key word here is "may", which to me means that my situation is valid according to the docs. So the question might become, under what conditions will calling setState, with the same value that is already set, cause a re-render? It would be useful to know this as one might want to put logic in front of their setState method to check if the value is the same as the current so as to not cause an erroneous re-render.

Vampiro
  • 103
  • 7
  • Each time you call `setBlah()` you are causing a re-render. With your original snippet you are calling `setBlah()` on every render, this makes your endless loop. The other `setBlah()` call is triggered off an event and not at render, which is why it does not cause the same loop. – Jacob Smit Nov 16 '21 at 21:55
  • Yes, I am not saying the `onClick`'s `setBlah` should cause the same loop - it should not. I am saying it's not causing ANY re-rendering to happen, not even once. – Vampiro Nov 16 '21 at 22:07
  • Since you've already set the default of blah to true - ```const [blah, setBlah] = useState(true)``` - mashing the button is not actually changing the state since it's already true. I think : ) – JDev518 Nov 16 '21 at 22:07
  • Totally, but why would that be different from the top level `setBlah(true)` and its causing endless re-renders when the value doesn't change? If top level `setBlah(true)` causes re-renders, which it does, endlessly, I would expect the button click to cause a single re-render. – Vampiro Nov 16 '21 at 22:15

2 Answers2

4

Via the documentation you linked to in your question:

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. (React uses the Object.is comparison algorithm.)

You can read there that React uses Object.is when comparing previous and new state values. If the return value of the comparison is true, then React does not use that setState invocation in considering whether or not to re-render. That is why setting your state value to true in the onClick handler doesn't cause a rerender.

That said, unconditionally calling a setState function at the top level of any component is always an error, because it initiates the reconciliation algorithm (infinitely). It seems to me that this is the core of your question, and if you want to learn about React Fiber (the implementation of React's core algorithm), then you can start here: https://github.com/acdlite/react-fiber-architecture

Here is another note from the React documentation (which needs to be updated for functional components):

You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.

https://reactjs.org/docs/react-component.html#componentdidupdate

Explaining how class component lifecycle methods translate to functional components is out of scope for this question (you can find other questions and answers on Stack Overflow which address this); however, this directive applies to your case.

Here's a snippet showing that your component only renders once when the erroneous setState call is removed:

<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.16.3/babel.min.js"></script>

<div id="root"></div>

<script type="text/babel" data-type="module" data-presets="react">

const {useRef, useState} = React;

function Example () {
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  const [bool, setBool] = useState(true);

  return (
    <div>
      <div>Render count: {renderCountRef.current}</div>
      <button onClick={() => setBool(true)}>{String(bool)}</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));

</script>
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • When you say `setState` at the top level of the component "initiates the reconciliation algorithm", is that not something that `setState` within the `onClick` handler does? I would have expected no reconciliation to happen for either as I would have imagined that `setState` would short circuit in both cases due to the value being the same as the default passed to `useState`, but that is obviously not happening for the top level `setState`, whereas it seems to happen for `onClick`'s `setState`. – Vampiro Nov 16 '21 at 23:12
  • 1
    I think you are confusing rendering and the reconciliation algorithm; they are not the same. Rendering can happen as a result of the reconciliation algorithm, but does not always occur. When you invoke a `setState` function (it doesn't matter where), it initiates the reconciliation algorithm, which, in turn, invokes your component function again. So, if you invoke `setState` at the top-level of your component, you are creating an infinite loop in runtime. – jsejcksn Nov 16 '21 at 23:31
  • 1
    I think most of your additional questions can be answered by reading the descriptive summary of React's Fiber architecture that I linked to in my answer. And, as always, you can find the most clarity by looking at the source code of React; here is a link to the `useState` hook in React v`17.0.2`: https://github.com/facebook/react/blob/17.0.2/packages/react/src/ReactHooks.js#L80. – jsejcksn Nov 16 '21 at 23:33
  • I've updated my answer with an additional quote from the React docs about calling `setState` conditionally. – jsejcksn Nov 16 '21 at 23:54
  • @Vampiro If this answers your question, feel free to mark it as such so that others can find it more easily. – jsejcksn Nov 17 '21 at 00:43
  • Thanks, I will read through the docs you sent. I would not expect reconciliation to occur in the event that we're calling `setState(value)` where value is what is already set - I'd have thought it'd just exit based on the new value being the same as the old before needing to reconcile/invoke my component function again. I'm sure there are reasons for this and perhaps they are somewhere in these docs. – Vampiro Nov 17 '21 at 11:30
  • I looked up the `useState` code earlier and came across this file which pointed me to another file which pointed me to another, etc. I'll have to pick up the search another day because of deadlines at the moment, as I definitely am curious to see it. – Vampiro Nov 17 '21 at 11:30
0

From my comment - Since you've already set the default of blah to true -

const [blah, setBlah] = useState(true);

Mashing the button is not actually changing the state since it's already true. However, if you do something like:

return <button onClick={() => setBlah(!blah)}>Blah</button>;

You'll see it switch between true and false in the console every time you hit the "Blah" button.

It's the

setBlah(true);

in the constructor that's causing the loop. It already set the state to true, then you're telling it to set it again in the same constructor.

JDev518
  • 772
  • 7
  • 16