React resetting and preserving state?

Viewed 86

By default, React preserves state of a component while it stays at the same position. Usually, this is exactly what you want, so it makes sense as the default behavior.

Examples are taken from the new React docs.

Example 1 - the same position, codesandbox, (relevant code) :

  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  ); 

Example 2 - different position, codesandbox, (relevant code):

  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );

I'm confused why the position in Example 2 is considered different? In both cases only one Counter component is rendered. What am I missing?

5 Answers

In the first example, both Counter will preserve the same state because the are rendered one at a time in the same exact position in DOM Tree. React thinks they are the same because the position. Latter by definition, Counter Taylor and Sarah are rendered at different location in DOM Tree. Therefore react thinks they are different and give them their own state.

Another option which produce same result as the second example is option 2 where even though they are switched in the same position, they have different keys therefore considered different.

Remember, react only sees component in DOM Tree and preserve their states only if they are rendered.

I would say, the final result is same but how they are implemented are different.

I am considering the examples right from the doc.

Example 1

{isPlayerA ? (
    <Counter person="Taylor" />
) : (
    <Counter person="Sarah" />
)}

In Taylor & Sarah, <Counter/> rendering of both is interrelated and here the relation is created by the ternary operator. I any case, only one will be shown as we do in else if or if else conditions.

For example:

{isPlayerA ? (
   <Counter person="Taylor" />
) : isPlayerB ? (
   <Counter person="Sarah" />
) : isPlayerC ? (
   <Counter person="Corner" />
) : (<Counter person="Jason" /> 
)}

There are interrealted as

whenever react finds any of the condition to be true, it will skip the rest checks. Like if it finds isPlayerB, then it will skip all the isPlayerC checks present in else section of playerB.

as in the image, the balance is the else if conditions. enter image description here


Example 2

{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}

In this case, the rendering is not interrelated, it is just that the variable deciding the rendering is same and thus you can say both are interrelated.

React will check all such conditions, no matter what to do final rendering. Even if it is like this

For example:

{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}
{isPlayerB &&
  <Counter person="Jason" />
}
{!isPlayerB &&
  <Counter person="Tyler" />
}

Here, react will check all the conditions, from 1 to 4, and the one who matches will be shown. If playerA === true, it will still check for !playerA and similarly for playerB as well.

As in the image, React is just striking out the elements that are not matching the conditions, there is not any kind of relation in between them.

enter image description here

This is very interesting :)

example 1 could be replaced like this.

  return (
    <div>
      <Counter person={isPlayerA ? "Taylor" : "Sarah"} />
      {/* isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      ) */}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  ); 

As you can see, <Counter /> doesn't be unmounted because only person props changed.

but example2 is different.

  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );

There are 2 <Counter /> and they are toggled mounted/unmounted

I believe it's because React treats the definition of both Counter as different elements through shallow equality. Remember in JS, {} === {} will equal false.

On the render where we changed players, it sees that the counter component of the previous render to be of a different instantiation through that if switch.

To preserve the position, you may add a key (should be the same value) on both components if you want the desired effect of preserving the state, or you know, just toggle the person props.

This has entirely to do with the node position within the tree hierarchy: if the same component or node type is not rendered at exactly the same position in the tree, React will consider it to be different. Other answers already cover this, so I'll focus on another important part:

In the reconciliation section of the React docs, the nature of this dilemma is discussed, and a solution is presented: the key attribute.

In order to solve this issue, React supports a key attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.

I won't copy and paste the entire documentation here, but you can read it at https://reactjs.org/docs/reconciliation.html#keys

Here is an example which demonstrates using a common key to help React maintain state across renders when the element position changes among its siblings:

body {
  font-family: sans-serif;
  padding-bottom: 2rem;
}

button {
  display: block;
  margin: 0.5rem 0;
}

code {
  background-color: hsla(0, 0%, 50%, 0.15);
  padding: 0.2em;
}
<div id="root"></div><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.4/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="react">

const {useState} = React;

function Counter ({person}) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <div>{person}</div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
    </div>
  );
}

function SameNodePosition () {
  const [isFirstPlayer, setIsFirstPlayer] = useState(true);
  const togglePlayer = () => setIsFirstPlayer(bool => !bool);
  return (
    <div>
      <h2>Same node position</h2>
      {
        isFirstPlayer ? <Counter person="Taylor" /> : <Counter person="Sarah" />
      }
      {/* Can also be written like this:
        <Counter person={isFirstPlayer ? 'Taylor' : 'Sarah'} />
      */}
      <button onClick={togglePlayer}>Toggle player</button>
    </div>
  );
}

function DifferentNodePosition () {
  const [isFirstPlayer, setIsFirstPlayer] = useState(true);
  const togglePlayer = () => setIsFirstPlayer(bool => !bool);
  return (
    <div>
      <h2>Different node position</h2>
      { isFirstPlayer && <Counter person="Taylor" /> }
      { !isFirstPlayer && <Counter person="Sarah" /> }
      <button onClick={togglePlayer}>Toggle player</button>
    </div>
  );
}

function DifferentSiblingNodePositionWithSameKey () {
  const [isFirstPlayer, setIsFirstPlayer] = useState(true);
  const togglePlayer = () => setIsFirstPlayer(bool => !bool);
  return (
    <div>
      <h2>Different sibling node position with same <code>key</code></h2>
      { isFirstPlayer && <Counter key="playerCounter" person="Taylor" /> }
      { !isFirstPlayer && <Counter key="playerCounter" person="Sarah" /> }
      <button onClick={togglePlayer}>Toggle player</button>
    </div>
  );
}

function DifferentNonSiblingNodePositionWithSameKey () {
  const [isFirstPlayer, setIsFirstPlayer] = useState(true);
  const togglePlayer = () => setIsFirstPlayer(bool => !bool);
  return (
    <div>
      <h2>Different non-sibling node position with same <code>key</code></h2>
      { isFirstPlayer && <Counter key="playerCounter" person="Taylor" /> }
      <div>
        { !isFirstPlayer && <Counter key="playerCounter" person="Sarah" /> }
      </div>
      <button onClick={togglePlayer}>Toggle player</button>
    </div>
  );
}

function Example () {
  return (
    <div>
      <SameNodePosition />
      <DifferentNodePosition />
      <DifferentSiblingNodePositionWithSameKey />
      <DifferentNonSiblingNodePositionWithSameKey />
    </div>
  );
}

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

</script>

Related