Remounting with key
Change a component's React key on purpose to remount it and reset its local state.
Picture a screen you have built before: a list of users on the left, and on the right a form to edit whichever one you click. You pick Alice, change her email, and before saving you click Bob. Bob’s name and role load into the form, but the email field still holds the half-typed address you were writing for Alice. You just leaked Alice’s edits onto Bob’s record.
This is reconciliation working as designed. When you learned how it matches elements across renders, you saw that React keeps the same component instance at a slot and feeds it new props. That is why the form’s local state survived the switch: the instance never went away, so its state never reset.
The fix is one attribute: <UserForm key={selectedUser.id} … />. The harder skill is knowing when to reach for it, and when three other tools fit better. Hold onto one idea throughout: a key is identity, and you get to choose it.
A key change is a remount, on purpose
Section titled “A key change is a remount, on purpose”Earlier in this chapter you learned that a component’s state and refs belong to its position + key in the tree. Keep the type, position, and key the same, and React reuses the instance across renders, handing it new props. Change any one of the three, and React performs a remount : it unmounts the old instance and mounts a brand-new one.
So far a remount was something to watch out for, the reason a list with the wrong keys put the typed value on the wrong row. Here you cause one on purpose. Same component, same position, but a different key tells React “this is a different instance now; throw the old one away.” It is reconciliation used as a state-reset switch.
A remount:
- resets local
useStateback to its initial value, - resets any refs,
- runs the old instance’s effect cleanup, then runs the new instance’s effects from scratch,
- and recreates the DOM nodes rather than patching them.
This work happens in Reconcile, on the Trigger → Render → Reconcile → Commit strip from earlier in the chapter. React reaches the slot and compares the new element’s key against the instance already there. The keys do not match, since bob is not alice, so React has nothing to reuse and builds a new instance.
Watch one slot across the selection change.
Render the slot holds Alice's instance.
<UserForm key="alice">
state email: "a@new…" (edited)<UserForm key="alice"> still mounted
state email: "a@new…" (edited)Reconcile key "alice" ≠ "bob" — no match to reuse.
<UserForm key="alice">
unmount email: "a@new…" discarded<UserForm key="bob">
mount email: "bob@corp…" (initial)Commit old DOM torn down, fresh node built.
<UserForm key="bob">
state email: "bob@corp…" (clean)A different key meant a different instance — so the form starts fresh.
The record-bound form, fixed with one line
Section titled “The record-bound form, fixed with one line”Let’s build the leaking form for real. Seeing the bug, the tempting-but-wrong fix, and the right fix next to each other is the fastest way to learn all three.
The setup is a minimal master-detail screen. A parent owns which user is selected and renders a child form for it:
const UserSettings = () => { const [selectedId, setSelectedId] = useState(users[0].id); const selectedUser = users.find((u) => u.id === selectedId)!;
return ( <div className="flex gap-6"> <ul> {users.map((u) => ( <li key={u.id}> <button onClick={() => setSelectedId(u.id)}>{u.name}</button> </li> ))} </ul> <UserForm user={selectedUser} /> </div> );};Clicking a name sets selectedId, re-rendering the parent so a different selectedUser flows as the user prop into the same <UserForm> slot.
Here is the child. It keeps the editable fields in local state, seeded from the user prop. That is the natural way to write this, and exactly the shape that leaks:
const UserForm = ({ user }: { user: User }) => { const [name, setName] = useState(user.name); const [email, setEmail] = useState(user.email);
return ( <form> <input value={name} onChange={(e) => setName(e.currentTarget.value)} /> <input value={email} onChange={(e) => setEmail(e.currentTarget.value)} /> <button type="submit">Save</button> </form> );};Read that initialization carefully. useState(user.email) does not mean “keep email in sync with the prop.” It means “use user.email as the initial value, the first time this instance mounts, and never again.” On every later render of the same instance, React ignores the argument: it already has a value stored for that slot and hands it back untouched.
Now trace switching from Alice to Bob. The parent re-renders with Bob’s user. React reaches the <UserForm> slot, sees the same component type at the same position with no key, and does what it promised: it reuses the instance and feeds it the new prop. But email still holds the string you typed for Alice. The useState(user.email) line runs again with Bob’s email as its argument, but that argument only mattered at mount, and mount already happened. State belongs to the slot, not to the user, and the slot did not change.
The fix you’ll reach for first, and why to put it down
Section titled “The fix you’ll reach for first, and why to put it down”Here is the move almost everyone makes before they know about key: watch the user’s id, and when it changes, push the new values into state by hand with an effect.
const UserForm = ({ user }: { user: User }) => { const [name, setName] = useState(user.name); const [email, setEmail] = useState(user.email);
useEffect(() => { setName(user.name); setEmail(user.email); }, [user.id]);
// …form};It works, and that is the trap, because working is not the bar. This fix is brittle. It uses one setter per field, so adding a phone field means remembering a fourth line, and forgetting it leaks phone across records again. It also runs one beat late: the effect fires after React has already rendered the form once with the stale state, so there is a flash of the wrong data before the correction lands. And it is the textbook shape of an anti-pattern you’ll meet in the effects chapter, under “you might not need an effect,” where resetting state because a prop changed is the canonical example. The rule to take on faith now: a useEffect that exists only to copy props into state is almost always the wrong tool.
The right fix deletes all of that and adds one attribute on the parent:
<UserForm key={user.id} user={user} />When the selection changes, user.id changes, so the key changes, so React remounts the form. Local state resets to the new user’s data: useState(user.email) runs as a real mount this time, so it uses Bob’s email. It does this for every field at once, with no per-field bookkeeping. Add a phone field tomorrow and it just works, because the key does not care how many fields live inside. The reset can never go stale, because no copy of the prop is being maintained, only a fresh instance born from the prop.
Put the three side by side: the bug, the fix to resist, and the one to keep.
<UserForm user={user} />The instance is reused across the switch, so the previous user’s edits leak onto the next record. No key means same type, same slot, so React keeps the old <UserForm> and feeds it Bob’s prop, while name and email still hold Alice’s typing.
const UserForm = ({ user }: { user: User }) => { useEffect(() => { setName(user.name); setEmail(user.email); }, [user.id]); // …};It works, but it’s brittle. One setter per field, an extra render with a flash of stale data before the correction lands, and it silently breaks the moment a teammate adds a field and forgets a line.
<UserForm key={user.id} user={user} />One attribute, declarative, scales to any number of fields for free. Changing user.id changes the key, so React remounts the form and local state is reborn from the new prop.
Now try it. The exercise below is the leaking form, live. Type into a field, click a different user, and watch your edits stick around where they don’t belong. Then fix it.
Type a new email for one user, then click a different user — your edits stick around on the wrong record. Add one attribute to <UserForm> so each user gets a fresh form.
Reference solution
<UserForm key={user.id} user={user} />Keying the form by user.id changes its identity on every selection, so React remounts it instead of reusing the instance. Each user gets a fresh form, with useState(user.email) running as a real mount and seeding the new record’s values.
One thing not to “fix” in that child: seeding state from props is the very thing that makes the pattern work. It looks like the cause of the bug, and in a sense it is, but it is also why the keyed version is so clean: the fresh instance is born already filled with the right data. Don’t reach for cleverer wiring to keep state and props in sync. The cleaner answer is to not own the state in the child at all, which is the next thing to weigh.
When the form should be controlled instead
Section titled “When the form should be controlled instead”A key reset suits a child that owns local state but must wipe it when its identity changes. Before reaching for it, ask whether the child should hold that state at all.
For many forms it should not, and the cleaner design is to let the parent own every field. Make the form a controlled component : the parent holds the values and passes them down, and the child becomes a presentational layer that renders what it is given and reports edits through callbacks. Now there is no local state in the child to reset. Switching records just rewrites the props, and the form follows along. No key needed.
A key reset earns its place when the draft has a real home in the child, a working copy the parent has no business tracking keystroke by keystroke. If the parent will track every keystroke anyway, own the state up there and let resets fall out of normal prop flow.
Here is the same form built both ways. Neither is universally right; the choice comes down to who owns the draft.
// Parent: keys the child by identity.<UserForm key={user.id} user={user} />
// Child: holds its own draft.const UserForm = ({ user }) => { const [name, setName] = useState(user.name); const [email, setEmail] = useState(user.email); // …};The child owns the draft, so resetting on identity change means changing the key. The parent never sees keystrokes; it only knows which user is selected, and the remount wipes the draft clean.
// Parent: holds the values, no key needed.const UserSettings = () => { const [name, setName] = useState(selectedUser.name); const [email, setEmail] = useState(selectedUser.email); return <UserForm name={name} email={email} onName={setName} onEmail={setEmail} />;};
// Child: presentational — renders props, reports edits up.const UserForm = ({ name, email, onName, onEmail }) => (/* … */);The parent is the single source of truth, so the form follows the props and resets come for free. The trade is a parent re-render on every keystroke: fine for small forms, and the right call when a sibling also needs the values.
Controlled forms and lifting state are subjects of their own later on. For now the point is narrow: before you reach for key, decide who should own the draft. If the answer is the parent, you have no reset problem at all.
Two more places the key reset applies
Section titled “Two more places the key reset applies”The record-bound form is the canonical case, but “new key means fresh instance” works anywhere a stateful subtree should start over. Here are two more examples, both the same key change applied to a different kind of state.
Replaying an animation on content change
Section titled “Replaying an animation on content change”A toast slides in with an entrance animation when it mounts. A new message arrives, you update the toast’s text, and the animation does not play. The text just changes in place. You never remounted: React kept the same instance and patched the text node, but the entrance animation runs on mount, and there was no mount. It fired once and never again.
The not-yet-played mount animation is a kind of state, and the cure is the same:
<Toast key={messageId} message={message} />Each new messageId is a key change, so React mounts a fresh toast with a brand-new DOM node, and the CSS entrance animation runs every time a node is created. The toast knows nothing about messages or replaying; it just mounts, and mounting is what plays the animation.
A reset button that bumps the key
Section titled “A reset button that bumps the key”A “Start over” button on a multi-step wizard, or a “Clear” on a search panel, should wipe the child’s state without unmounting the surrounding page or threading a reset signal through every field by hand.
There is nothing natural to key by here: no record id changes, no message arrives. So you manufacture an identity. Hold a counter in the parent, key the child by it, and bump the counter on click. Each bump is a new key, which remounts the child for a clean slate.
const SearchPanel = () => { const [resetKey, setResetKey] = useState(0);
return ( <section> <SearchForm key={resetKey} /> <button onClick={() => setResetKey((k) => k + 1)}>Start over</button> </section> );};A counter in the parent. This integer is the form’s identity; its only job is to change on reset.
const SearchPanel = () => { const [resetKey, setResetKey] = useState(0);
return ( <section> <SearchForm key={resetKey} /> <button onClick={() => setResetKey((k) => k + 1)}>Start over</button> </section> );};The form is keyed by that counter. While resetKey holds steady, the form keeps its state across the parent’s re-renders, like any keyed child.
const SearchPanel = () => { const [resetKey, setResetKey] = useState(0);
return ( <section> <SearchForm key={resetKey} /> <button onClick={() => setResetKey((k) => k + 1)}>Start over</button> </section> );};Clicking bumps the counter with the updater form (next-depends-on-prev, from the snapshot lesson earlier in this chapter). A new value is a new key, which remounts the child no matter how many fields live inside.
This is the record-bound form’s argument from the other direction: one mechanism resets all the descendant state at once, with nothing to clear per field and nothing that drifts as the form grows. The whole moving part is a single integer.
Choosing the right reset: key, lift, derive, or control
Section titled “Choosing the right reset: key, lift, derive, or control”The skill here is not the trick but the order in which you weigh your options, so that key ends up your last reach instead of your first.
There are four tools, each with its own trigger:
keyreset. The child owns local state (a draft) that must reset when the identity it is bound to changes — a record, a message, or a session the parent already tracks.- Lift the state to the parent. A sibling needs to read it, or the parent should be the single source of truth. The child becomes controlled and resets fall out of prop flow.
- Derive it in render. The value is computable from props or other state, so it is derived state : don’t store a copy, just compute it while rendering. This is the antidote to “I stored a prop in state and now they’re out of sync.”
- Persist, and do nothing. The state is supposed to survive the identity change: scroll position, expanded panels, an in-progress sub-selection. Resetting it would be the bug.
Two mistakes follow from treating key as the first option: reaching for it when deriving or lifting is cleaner, and resetting state the user wanted kept. Working through the questions first avoids both.
Leave the state alone. Resetting a scroll position or an expanded panel the user opened would throw away work they expect to keep, which is the bug, not the fix.
Don’t store a copy; compute it from props or other state while rendering. This is the antidote to “I stored a prop in state and now they’re out of sync.” (Covered next chapter, on state.)
Move the state up; the child becomes controlled and resets fall out of prop flow. Reach here when a sibling reads it or the parent should own the truth. (Covered next chapter, on state.)
Key the owning subtree by the identity it’s bound to: key={record.id} or a counter you bump on purpose.
The right reach, and the last one, when the child owns a draft that must reset on an identity change.
Walk the questions in order; key is the last answer you reach, not the first.
Once you land on key, two practical notes follow. First, account for the cost. A remount runs every effect’s cleanup and re-runs the effects (a subscription or socket tears down and re-establishes), refs re-attach, animations replay, and the DOM is recreated. For most components this is imperceptible; for a heavy subtree with expensive mount logic it is measurable, so know you are paying it and decide it is worth paying.
Second, key the smallest subtree that owns the state you want to reset. Put the key on the top component of the reset target, not on a deep leaf (you would reset only that leaf) and not on a far-up ancestor (you would discard more than you meant to). The key wraps exactly what resets, so choose its scope as deliberately as you choose when it changes.
Two ways to break a key reset
Section titled “Two ways to break a key reset”This tool has two failure modes worth pinning down.
The first is an unstable key, which remounts the component on every render and leaves it unusable. Watch what happens when the key is computed fresh on every render:
<Form key={Math.random()} />A new key every render means React remounts every render: state can never persist, effects thrash, and the input loses focus mid-keystroke. The component is effectively unusable.
<Form key={user.id} />The key changes only on the intended identity switch, so the form persists between switches and resets exactly when you mean it to.
Math.random() and Date.now() return a different value on every render, so the key changes constantly and React remounts the form constantly. The rule that keeps you safe: the key must change only when the reset is intended. Derive it from a stable identity (user.id) or a counter you bump on purpose, never from a random or time-based value. This is the array-index list bug from earlier, seen from the reset angle instead of the list angle.
To see the loop rather than just read about it, predict the output below. Child logs mounted from an effect that runs once when it mounts (effects are a later chapter; take that as given), and the parent re-renders itself three times.
Child logs 'mounted' when it mounts. The parent re-renders itself until n reaches 2 — three renders in all. What does the console print? Predict what this program prints, then press Check.
const Child = () => { useEffect(() => console.log('mounted'), []); return <p>hi</p>;};
const Parent = () => { const [n, setN] = useState(0); useEffect(() => { if (n < 2) setN(n + 1); }); return <Child key={Math.random()} />;};An inline Math.random() key is a new value on every render, so React unmounts and remounts Child each time — re-running its mount effect on all three renders. Give Child a stable key (key="child") and React mounts it once: you’d see mounted a single time, no matter how often the parent re-renders.
That repeated mounted is the signal to watch for: a component you expected to mount once mounting again and again means an unstable key is feeding it a new identity every render.
The second failure is resetting state the user wanted to keep. A key change discards every piece of local state in the subtree it wraps — including a scroll position, an expanded panel, or a sub-selection the user was in the middle of. So be deliberate about what the key wraps: if only part of the subtree should reset, key only that part. This is the “key the smallest owning subtree” rule again, now guarding state the user expected to survive.
One practical note: a remounted child starts from useState’s initial value, so if the fresh instance should come up pre-filled, pass the data through props and initialize from it with useState(user.email). (For an initial value that is expensive to compute, a lazy form useState(() => …) exists, covered in the next chapter.)
Where to go deeper
Section titled “Where to go deeper”The canonical treatment of position + key identity and the key-reset pattern — the single best follow-up to this lesson.
Why the effect-that-clears-fields fix is the wrong reach — resetting state on a prop change is one of its named anti-patterns.
Kent C. Dodds builds intuition for keys by reasoning from React's point of view — the 'why' behind the identity rule this lesson leans on.
A key is identity you choose: change it when a stateful component’s identity changes, and reach for it only after persisting, deriving, and lifting are ruled out.