Topic 6.4
Preserving and Resetting State
In one line
React keeps a component's state as long as the same component type is rendered at the same position in the tree; a different type or a removed component throws the state away, and changing a component's key resets it on purpose.
Think of it like this
Lockers at a swimming pool. Your things are kept in locker number 7, not tied to your face. If you leave and someone else is given locker 7, they find your things inside. If the pool replaces locker 7 with a bench, everything in it is thrown out. React stores state the same way: by position in the tree, not by which variable or which person the component is "about".
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- UI tree
- React's tree of component instances and elements built from your JSX. State is attached to its positions.
- Instance
- One copy of a component on screen. Two
<Counter />tags make two instances with separate state. - Position
- Where a component sits in the tree: which parent, and which child slot.
- Preserve
- React keeps a component's state between renders.
- Reset
- React throws the state away and the component starts again from its initial values.
- Mount / unmount
- Mount: a component is created and added to the screen. Unmount: it is removed and its state is destroyed.
- key
- A special prop that tells React which component is which. A new key means a new component.
Step by step
01State lives at a position
Two Counter tags make two separate slots in React's tree. Each slot has its own score. Clicking one doesn't touch the other, because they sit at different positions.
02Same type, same position: kept
fancy ? <Counter fancy={true} /> : <Counter fancy={false} /> looks like two different counters in your code. In the tree it is one Counter at the first child slot either way, so its state survives the switch. Only the prop changes.
03Different type, or removed: reset
Swap the wrapping div for a section and React sees a different type at that position. It destroys the whole subtree, Counter included, and builds a new one with fresh state. Removing a component does the same: when it comes back, it starts from zero.
04The scoreboard trap
isAsha ? <Counter person="Asha" /> : <Counter person="Ravi" /> is one Counter at one position, so Ravi inherits Asha's score. Two fixes: render them at different positions ({isAsha && <Counter person="Asha" />}{!isAsha && <Counter person="Ravi" />}), or give them different keys.
{isAsha ? (
<Counter key="Asha" person="Asha" />
) : (
<Counter key="Ravi" person="Ravi" />
)}05Reset a form with key
A messenger shows one Chat for the selected contact. Without a key, the draft you typed to Asha is still there when you open Ravi, and you might send it to the wrong person. key={contact.id} makes each contact's chat a separate instance, so switching starts with an empty box.
<ContactList contacts={contacts} selectedId={to.id} onSelect={setTo} />
<Chat key={to.id} contact={to} />06Keep drafts when the chat is gone
To remember each contact's draft even though the Chat is destroyed, move the draft up: the parent keeps drafts as an object keyed by contact id, and Chat becomes controlled. The state no longer lives in the component that gets thrown away.
const [drafts, setDrafts] = useState({}); // { 1: "Hi Asha", 2: "" }
<Chat
key={to.id}
contact={to}
draft={drafts[to.id] ?? ""}
onDraftChange={(text) => setDrafts({ ...drafts, [to.id]: text })}
/>Try it yourself
- 1
Predict, then flip the checkbox
In the first example, click the first counter twice by hand and tick Fancy styling. Predict the score before you look. Then change the fancy branch to
<p><Counter fancy={true} /></p>(an extra wrapper only in one branch). Predict again: is the score kept now? (No: the type at that position changed fromCountertop.) - 2
Fix the scoreboard without keys
In the scoreboard, replace the ternary with
{isAsha && <Counter person="Asha" />}{!isAsha && <Counter person="Ravi" />}. Each player now has their own position (an empty slot is still a slot), so the scores no longer leak. - 3
Hide instead of removing
In the key chat, render one
<Chat>per contact (contacts.map, each withkey={c.id}) and give the inactive oneshidden={c.id !== to.id}. Drafts survive switching. What is the cost? (Every chat stays mounted and in the DOM.)
Code & diagrams
The script adds 3 to both counters. Turning on fancy styling keeps the first score (same type, same position). Hiding the second counter unmounts it, so when it comes back it starts at 0.
Preview
Console
Nothing logged yet.Expected console output
After three clicks each: 3, 3
Fancy on, second hidden: 3
Second shown again: 3, 0`Counter` itself never changed, but its parent went from `div` to `section`. React rebuilt that whole subtree, so the score reset to 0.
Preview
Console
Nothing logged yet.Expected console output
Score before: 2
Score after the wrapper changed: 0Without a key, Ravi steals Asha's 3 points: it's the same `Counter` at the same position with a new prop. With `key`, React sees a different component and Ravi starts at 0. (Switching back to Asha also starts at 0: her instance was destroyed. The last example shows how to keep it.)
Preview
Console
Nothing logged yet.Expected console output
Without key -> Ravi: 3
With key -> Ravi: 0Type a message to Asha, then click Ravi: the box is empty, because the new key made a new `Chat`. Delete `key={to.id}` and try again: your draft to Asha now sits in Ravi's box.
Preview
Console
Nothing logged yet.Write to Ravi, switch to Asha and back: both drafts survive, and a ✎ marks contacts with an unsent draft. The `Chat` is still destroyed on each switch; its data simply lives in the parent.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Define a component inside another
In the last example (drafts lifted up), move function Chat(...) { ... } inside Messenger, just above its return. Click in the box and type a word.
Break #2
Use the array index as the chat key
Render the chat as <Chat key={0} contact={to} /> (a key that never changes).
Myth vs fact
Myth
State lives inside the component function.
Fact
React stores it in its own tree, attached to the component's position. Your function just reads it with useState each time it is called.
Myth
Two different JSX lines always make two different components.
Fact
React compares the resulting tree, not your source. Both branches of a ternary that render the same type at the same spot are the same component to React.
Myth
key is only for lists.
Fact
Any component can take a key. Changing it is the standard way to reset a component's state.
Myth
A hidden component loses its state.
Fact
Only a removed one does. Hiding with CSS or hidden, or React 19.2's <Activity mode="hidden">, keeps it mounted with its state.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Under the hood the state slot is a fiber (Topic 24.1). Reconciliation compares
typeandkeyof the new element with the existing fiber at each position: same type and key means the fiber is reused and updated; otherwise the old fiber subtree is deleted (running cleanups) and a new one is created. This is the O(n) heuristic of Topic 16.2. - ▸
Unmounting doesn't only reset state: it removes the DOM nodes (losing focus, scroll and selection), runs effect cleanups and detaches refs. Changing a key on a large subtree is therefore more expensive than updating it; use it when a fresh start is what you want.
- ▸
{cond && <A />}leaves a hole (false) at that position, which still counts as a slot. That is why{a && <X />}{!a && <X />}creates two positions whilea ? <X /> : <X />creates one. - ▸
<Activity mode="hidden">(React 19.2) keeps a subtree's state and DOM while hiding it and unmounting its effects, which is the built-in answer for tabs and back navigation (Topic 16.10). Before it, hiding with CSS or lifting state up were the options.
Remember this
- 1
State is not stored inside your component function. React keeps it in its own tree, one slot for each component instance (each
<Counter />on screen). That slot belongs to the component's position in the UI tree: "the first child of themaininsideApp". On every render, React compares the new tree with the old one, position by position (reconciliation, Topic 0.2), to decide whether each slot is kept. - 2
Same type at the same position keeps the state. If the previous render had a
Counterat a position and this render also has aCounterthere, React treats it as the same component and keeps its state, even if its props changed. This is true even if your JSX writes it in two different places, such as both branches of a ternary:fancy ? <Counter fancy /> : <Counter />. React doesn't see your code, only the tree that came out of it. - 3
A different type at the same position resets the state. If a
Counteris replaced by a<p>, or a<div>wrapping it becomes a<section>, React tears down the old subtree, destroying the state of everything inside it, and builds the new one from scratch. The same happens when a component is removed ({show && <Counter />}withshowfalse): its state is gone, and showing it again starts from the initial values. - 4
This is why you must never define a component inside another component. A function declared inside
Appis a brand-new function on every render, so React sees a different type at that position each time and resets its state (and its DOM: inputs lose focus) on every render. - 5
**Resetting with
key.** Sometimes you want the opposite: same type, same position, but a fresh start. A chat window should clear the draft when you switch to another contact. Give the component akey:<Chat key={contact.id} contact={contact} />. Keys aren't only for lists (Topic 3.3); React treats a different key as a different component, so the old instance and its state are thrown away and a new one starts. Keys are compared among siblings only, not globally. - 6
Keeping the state of removed components. Sometimes you want state to survive while a component is off screen: drafts per contact, a half-filled tab. Options: render all of them and hide the inactive ones with CSS (simple, but every one stays in the DOM), lift the state up into the parent, keyed by id, so the child can be destroyed safely (the usual answer), save it outside React (for example
localStorage), or use React 19.2's<Activity mode="hidden">, which hides a subtree while keeping its state (Topic 16.10).
Explain it without notes
Where does React store a component's state, and what decides whether it's kept between renders?
Why does isA ? <Counter person="A" /> : <Counter person="B" /> keep the same state, and how do you fix it?
What happens to state when a component's parent changes from a div to a section?
Why must you never declare a component inside another component?
How can you keep a component's data when the component is removed?
Practice
A profile page renders <EditProfileForm user={user} />. When the user picks another person from a list, the form still shows the previous person's edits. Fix it with one change.
Write a Tabs parent that shows either <Notes /> or <Todos /> (each with its own useState text box) and keeps both boxes' text when switching tabs, without lifting state up.
Trade-offs
- ↔
Resetting with
keyis simple and complete, but it rebuilds the subtree's DOM and runs all cleanups; for small forms that's nothing, for huge subtrees it can be noticeable. - ↔
Keeping hidden components mounted preserves state for free but keeps their DOM and memory alive; lifting state up keeps only the data but needs more wiring.
Done when you can
I can explain that state is tied to a position in the tree.
I can predict when React keeps or resets state when the JSX changes.
I can reset a component on purpose with
key.I never declare components inside other components.
I can keep data for removed components by lifting it up or hiding instead of removing.