Skip to content
Chapter 23Lesson 5

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.

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 useState back 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.

slot: detail pane

<UserForm key="alice">

state email: "a@new…" (edited)
Render A — Alice is selected. The form instance in the detail slot holds her half-typed edits in local state.
new element <UserForm key="bob">
slot: detail pane

<UserForm key="alice"> still mounted

state email: "a@new…" (edited)
Selection changes to Bob. Render B produces a new element — same slot, but the key is now "bob". The Alice instance is still sitting in the slot from the last render.

Reconcile key "alice" ≠ "bob" — no match to reuse.

slot: detail pane

<UserForm key="alice">

unmount email: "a@new…" discarded

<UserForm key="bob">

mount email: "bob@corp…" (initial)
Reconcile — React compares keys. alice ≠ bob, so there is nothing to reuse: the Alice instance unmounts (its state is discarded) and a fresh Bob instance mounts with state at its initial value, from props.

Commit old DOM torn down, fresh node built.

slot: detail pane

<UserForm key="bob">

state email: "bob@corp…" (clean)

A different key meant a different instance — so the form starts fresh.

Commit — the old DOM node is torn down and a new one built. The form now shows Bob's data with no leaked edits.

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:

UserSettings.tsx
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:

UserForm.tsx
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.

UserForm.tsx
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.

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.

Preview LIVE
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.

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.

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.

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 “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.

1 / 1

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:

  • key reset. 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.

Which reset reach?

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.

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.

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()} />;
};

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.)

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.