Setting and Preserving State
From beta.reactjs.org, preserving and resetting state
For each component, React associates its state with that component's location in the UI tree.
React preserves a component’s state for as long as that same component is being rendered at the same position in the UI tree.
Same component at the same position preserves state
When a component gets removed, or a different component gets rendered at the same position, React destroys its state.
...
// The Counter's state is preserved, whether fancy or not.
// In React's UI tree, it sits just under <div> in both cases
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
</div>
)
...
...
// The Counter's state is NOT preserved!
// 2nd instance is at different place in the UI tree
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<div>
<Counter isFancy={false} />
</div>
)}
</div>
)
...
One can think of them as having the same “address”, e.g., "the first child of the first child of the root". This is how React matches them up between the previous and next renders, regardless of how you structure your logic. React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return.
Also, rendering a component under a different UI element--even if it's in the same relative position under that different UI element--resets the state of the component's entire subtree:
...
// The Counter's state is destroyed if isFancy changes.
// In React's UI tree, sitting just under <div> is a different
// position than sitting just under <section>
return (
{isFancy ? (
<div>
<Counter />
</div>
) : (
<section>
<Counter />
</section>
)}
)
...
This is why you should not nest component function definitions:
export default function MyComponent() {
const [counter, setCounter] = useState(0);
// Every time MyComponent renders, a *different*
// MyTextField() function is being created, which
// means its state is lost.
function MyTextField() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField />
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
What if you want state to change on re-render?
Two approaches:
- Put component in different position
- Give each component an explicit identity with
key
Putting same component in different position resets state
This example illustrates a very tricky behavior. React creates positions for TWO <Counter />
components even though one position is empty due to the isFancy condition. Therefore, state is destroyed when isFancy changes and the component is removed from one position and added to the other--but the position is there regardless.
export default function Example() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{ isFancy && <Counter isFancy={isFancy} /> }
{!isFancy && <Counter isFancy={isFancy} /> }
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => { setIsFancy(e.target.checked) } }
/>
Use fancy styling
</label>
</div>
);
}
Giving same component different identity with key
attribute resets state
// Resets state
...
return (
<div>
{isFancy ?
<Counter isFancy={isFancy} key="A" /> :
<Counter isFancy={isFancy} key="B" />
}
</div>
)
...
(NOTE: A ternary operator can live inside a JSX expression, but an if
statement cannot; the if
must go outside the return()
.)
// Does NOT reset state. (The `id` prop does not differentiate components)
...
return (
<div>
{isFancy ?
<Counter isFancy={isFancy} id="a"/> :
<Counter isFancy={isFancy} id="b"/>
}
</div>
)
...
Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent.
Keys are not globally unique. They only specify the position within the parent.
Also note that the above examples are working with drawing one component versus another; if multiple components are being drawn, then a key
field will serve to assign state to the correct component...see next example.
For multiple instances of same component, the key
field helps React assign correct state across re-renders
export default function App() {
const [reverse, setReverse] = useState(false);
// Cool, assigning a component to a variable!
let checkbox = (
<label>
<input type="checkbox" checked={reverse}
onChange={e => setReverse(e.target.checked)} />
Reverse order
</label>
);
if (reverse) {
return (
<>
<Field label="Last name" key="L" />
<Field label="First name" key="F" />
{checkbox}
</>
);
} else {
return (
<>
<Field label="First name" key="F" />
<Field label="Last name" key="L" />
{checkbox}
</>
);
}
}
function Field({ label }) {
const [text, setText] = useState('');
return (
<div>
{label}:
<input type="text" value={text} placeholder={label}
onChange={e => setText(e.target.value)} />
</div>
);
}
Summary
- React keeps state for as long as the same component is rendered at the same position.
- State is not kept in JSX tags. It’s associated with the position of that JSX in the tree.
- A
key
lets you specify a named position instead of relying on order. - You can force a subtree to reset its state by giving it a different key.
- Don’t nest component definitions, or you’ll reset state by accident.