reactjs

How does React determine which state to apply when children are conditionally rendered?


React maintains its states by storing the order of useState calls and fill in the states with that order during the next re-rendering.

But when there is a conditional re-rendering, like having a boolean flag to control whether a child will be shown or not, this order is broken.

import { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
      <button onClick={() => setCount(count + 1)}>
        Click to increment: {count}
      </button>
  );
};

const CounterGroup = ({ visible }) => {
  // visible controls whether the first counter is displayed
  return (
      <div>
        { visible && <Counter/> }
        <Counter/>
      </div>
  );
};

const App = () => {
  const [visible, setVisible] = useState(true);

  return (
      <div>
        <CounterGroup visible={ visible }/>
        <p>
          <button onClick={ () => setVisible(!visible) }>
            Toggle Counter
          </button>
        </p>
      </div>
  );
};

export default App;

In this example, when rendering App, 3 states are used: one visible and two count's. But when visible is set to false, there are only one visible and one count state to fill in. How does React know which count is the one to preserve (which is the second apparently)?


Solution

  • React maintains the order of useState calls during the rendering of each component, instead of storing a global order.

    Rendering process

    In the example of the question, the rendering process is something like this:

    // Step 1
    <App/>
    
    // Step 2
    // found one useState call
    // storing the state value: true
    <div>
      <CounterGroup visible={true}/> // visible is true
      <p> ... </p>
    </div>
    
    // Step 3
    <div>
      <div>
        <Counter/>
        <Counter/>
      </div>
      <p> ... </p>
    </div>
    
    // Step 4
    // found one `useState` use in each <Counter/>
    <div>
      <div>
        <button> ... </button>
        <button> ... </button>
      </div>
      <p> ... </p>
    </div>
    

    Now you can click on the buttons a few times and then click the toggle button. When visible is set to false, the key difference is at step 3. You may think the re-rendered result is:

    // Step 3
    <div>
      <div>
        <Counter/>
      </div>
      <p> ... </p>
    </div>
    

    If so, React won't know which <Counter/> is left. However, JSX grammar { ... } actually always provides something. When visible is false, the result of the JSX expression is false, instead of nothing. So the resulting tree is actually:

    // Step 3
    <div>
      <div>
        { false }
        <button> ... </button>
      </div>
      <p> ... </p>
    </div>
    

    Therefore, the number of children does not change.

    DOM Difference

    Since React stores states for each component, it needs to compare the DOM tree resulted from two renderings, and determine the correspondence between components.

    In the example above, there are two different DOM trees in step 3. To be simple, only the different part is written:

    // Before
    <Counter/>
    <Counter/>
    
    // After
    { false }
    <Counter/>
    

    By default, React makes components with the same position correspond. That is, the first counter corresponds to { false }, and the second one corresponds to <Counter/>:

    <Counter/> => { false }
    <Counter/> => <Counter/>
    

    Since { false } has different type from <Counter/>, this correspondence does not make sense, so being ignored. The second correspondence does make sense, so React fill in the states of <Counter/> during the re-rendering process with those of the second counter.

    React Key

    React key is something that you can tell React how to correspond the children. By setting the key of a component, React will correspond the components with the same key instead of comparing the position index.

    When you are using .map to create a number of children, React recommends you to use key property, since using position index to identify components is usually not the expected behavior.

    Another example

    If you do not use JSX grammar, but use if-else blocks instead, you will get something different:

    const CounterGroup = ({ visible }) => {
      // visible controls whether the first counter is displayed
      if (visible) {
        return (
            <div>
              <Counter/>
              <Counter/>
            </div>
        );
      } else {
        return (
            <div>
              <Counter/>
            </div>
        );
      }
    };
    

    This time, there is no { false } component being rendered. So the DOM diff will be:

    <Counter/> => <Counter/>
    <Counter/> => // Nothing here
    

    So the states of the first counter will be applied during re-rendering.