Remounting with key
Use React's key prop as a deliberate switch to remount a component and reset its state on purpose.
Picture a screen you have built a dozen times without thinking about it: a list of users down the left, and on the right a form to edit whichever one you click. You pick Alice, change her email to something new, and before you save you click Bob. Bob’s name loads into the form. His role loads. 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.
That is not a bug in your form. It is the behavior you met earlier in this chapter, working exactly as designed. When you learned how reconciliation matches elements across renders, you saw that React keeps the same component instance at a slot across renders and just feeds it new props. That is precisely why the form’s local state survived the switch from Alice to Bob: the instance never went away, so its state never reset.
The fix is one attribute: <UserForm key={selectedUser.id} … />. But the attribute is the easy part. The real skill is knowing when to reach for it, and when three other tools fit better. That is what this lesson is about. Hold onto one idea the whole way through: 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”You already know the mechanism. 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 those three, and React performs a remount : it unmounts the old instance and mounts a brand-new one.
Back then, a remount was something that happened to you: a thing to watch out for, the reason a list with the wrong keys put the typed value on the wrong row. Here you turn it around and cause a remount on purpose. Same component, same position, but a different key tells React, in its own terms, “this is a different instance now; throw the old one away.” It is reconciliation, used as a state-reset switch.
That switch is not free, so it is worth being precise about what it does the moment you flip it. 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 is the same list you saw when a remount happened by accident. Nothing new is going on here; the only thing that changed is your intent, because this time you asked for it.
Place this on the phase strip from earlier in the chapter, Trigger → Render → Reconcile → Commit. The key change does its work in Reconcile. React reaches the slot and compares the new element’s key against the instance that was there. The keys do not match, since bob is not alice, so React has nothing to reuse and builds a new instance. The reset is not a side effect you arranged; it is the natural consequence of React looking for the old instance and, by your design, not finding it.
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.
That is the entire mechanism. Everything from here is application: learning to recognize the situations where flipping this switch is the right move, and the situations where it is the wrong one.
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. It is the pattern this whole lesson orbits, and seeing the bug, the tempting-but-wrong fix, and the right fix next to each other is the fastest way to internalize all three.
The setup is a minimal master-detail screen. A parent component 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, which re-renders the parent with a different selectedUser flowing as the user prop into the same <UserForm> slot.
And here is the child. It keeps the editable fields in local state, seeded from the user prop. That is exactly the natural way you would write this, and exactly the shape that creates the leak:
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, because it is the hinge of the whole bug. useState(user.email) does not mean “keep email in sync with the prop.” It means “use user.email as the initial value, the very first time this instance mounts, and never again.” The setter takes over from there. On every later render of the same instance, the useState argument is ignored entirely: React already has a value stored for that slot and hands it back to you untouched.
So trace what happens when you switch 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 exactly what it promised: it reuses the existing instance and feeds it the new prop. But the instance is not new. Its email state is still the string you typed for Alice, because nothing told React to start over. The useState(user.email) line runs again and sees 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 here. This fix is brittle in a way that will catch a future teammate. It uses one setter per field, so the day someone adds a phone field and forgets to add a fourth line, phone starts leaking across records again, and nobody knows why until a customer complains. 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 will study in the chapter on effects, under the banner “you might not need an effect,” where resetting state because a prop changed is the canonical example. You do not need to know effects to take this 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 replaces it with 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 actually uses Bob’s email. It does this for every field at once, with zero per-field bookkeeping. Add a phone field tomorrow and it just works, because the key does not know or care how many fields live inside. The reset can never go stale, because there is nothing to keep in sync: no copy of the prop is being maintained, only a fresh instance born from the prop.
Put the three side by side. The first column is the bug, the second is the fix you should resist, the third is the senior default.
<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 just 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 and effect-smelly. 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. This is the default an experienced engineer reaches for.
Now try it yourself. The exercise below is the leaking form, live. Type into a field, click a different user, and watch your edits stick around where they do not 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 this pattern work. It looks like the cause of the bug, and in a sense it is, but it is also the reason the keyed version is so clean, because the fresh instance is born already filled with the right data. Do not reach for some cleverer wiring to keep state and props in sync. If anything, 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 is the right tool for a child that legitimately owns local state but needs to wipe it when its identity changes. Everything turns on that word “legitimately.” Before you reach for the key, ask whether the child should be holding that state in the first place.
For a whole class of forms the answer is no, and the cleaner design is to let the parent own every field. Make the form a controlled component : the parent holds the values, passes them down, and the child becomes a thin presentational layer that renders what it is given and reports edits back through callbacks. Do that, and there is no local state in the child to reset. Switching records just rewrites the props the parent passes, and the form follows along for free. No key needed, because there is nothing to throw away.
Here is the heuristic an experienced engineer carries, stated plainly:
The key reset earns its place when the local state has a real home in the child, a working draft the parent has no business tracking keystroke by keystroke, but still needs to reset on an identity change. If the parent is already going to track every keystroke anyway, skip the middleman: own the state up there and let resets fall out of normal prop flow.
Look at the same form built both ways. Neither is universally correct. The whole decision comes down to a single question: 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 working 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 that the parent re-renders on every keystroke. That is fine for small forms, and the right call when a sibling also needs the values.
The full controlled-form patterns, wiring value and onChange, validating, and submitting, are their own subject later in the course, when you reach forms and server actions. And lifting state into a parent is itself a deliberate move with its own rules, which you will study in the next chapter, on state. For now the point is narrow: before you reach for key, decide who should own the draft. If the answer is “the parent,” you do not have a reset problem at all.
The same trick, two more faces
Section titled “The same trick, two more faces”The record-bound form is the canonical case, but the mechanism is generic: “new key means fresh instance” applies anywhere a stateful subtree should start over. Once you can see it in the form, you can spot it everywhere. Here are two more places it shows up, wearing different costumes. Do not look for a new mechanism in either; it is 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”Say you have a toast that slides in from the corner with a little entrance animation when it mounts. A new message comes in, you update the toast’s text, and the animation does not play. The message just changes in place, silently. The reason is that you never remounted. React kept the same toast instance and patched the text node inside it, but the entrance animation runs on mount, and there was no mount. It fired once, the first time, and never again.
The not-yet-played mount animation is a kind of state, and the cure is the same one:
<Toast key={messageId} message={message} />The entrance is a CSS animation guarded by motion-reduce: so it respects a reduced-motion preference, but the styling is beside the point here; the lesson is the key, not the animation.
Each new messageId is a key change, so React mounts a fresh toast with a brand-new DOM node, and a CSS entrance animation runs every time its node is created. The toast component knows nothing about messages or replaying animations. It just mounts, and mounting is what plays the animation. The remount did the work.
A reset button that bumps the key
Section titled “A reset button that bumps the key”The second costume is a reset button. You have a “Start over” button on a multi-step wizard, or a “Clear” on a search panel, and clicking it should wipe the child’s state clean 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, and 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, which gives you 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, starting at 0. This integer is the form’s identity, and its only job is to change when you want a 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, just 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 gives a new key, which remounts the child for a clean slate, no matter how many fields live inside.
Notice this is the same argument as the record-bound form, just from the other direction. One mechanism resets all of the descendant state at once, with no per-field clearing, nothing to keep in sync, 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”This is the judgment that separates a junior from a senior here. A junior learns “bump the key” and then bumps it everywhere, including the many places where a cleaner tool fits. The durable skill is not the trick itself but the order in which you consider your options, so that the key ends up being your last reach instead of your first.
There are four tools, each with its own trigger:
keyreset. The child legitimately owns local state (a draft) that must reset when the identity it is bound to changes. That identity is something the parent already tracks: a record, a message, or a session.- Lift the state to the parent. A sibling needs to read this state, or the parent should be the single source of truth. The child becomes controlled, and resets fall out of prop flow. (Its own subject in the next chapter, on state.)
- Derive it in render. The value is computable from props or other state, so it is derived state : don’t store a copy of it at all, just compute it while rendering. This is the antidote to “I stored a prop in state and now they’re out of sync.” (Also covered in the next chapter, and again in the chapter on effects.)
- Persist, and do nothing. The state is supposed to survive the identity change: scroll position, which panels the user expanded, an in-progress sub-selection. Resetting it would be the bug, not the fix.
Two mistakes are worth calling out: reaching for key when deriving or lifting is cleaner, and resetting state the user wanted kept. Both are common, and both come from treating key as the first option instead of working through the others before it.
So walk the questions in order. The diagram below is interactive. Click through it the way an experienced engineer actually thinks, one question at a time, and notice that key is the last thing you arrive at, not the first.
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.
Once you have landed on key, two practical notes follow. First, account for the cost honestly. A remount runs every effect’s cleanup and then re-runs the effects (a subscription or socket inside the subtree 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 that you are paying it and decide it is worth paying. This is a trade-off to weigh, not a rule against using key.
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 miss the rest) and not on some far-up ancestor (you would discard far more than you meant to). The key wraps exactly what resets. 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, both particular to it, and both worth pinning down before you ship anything with a key reset in it.
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() produce a different value every single time render runs. So the key changes on every parent render, which means React remounts the form on every parent render. The state can never persist long enough to be useful, effects tear down and rebuild constantly, and the input loses focus mid-keystroke. The component is, functionally, broken. 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 value that is freshly random or time-based each render. This is the same hazard you saw when an array index made a bad key, viewed from the reset angle instead of the list angle.
Reading about the loop is one thing and seeing the output is another. In the drill below, Child is rendered with an unstable key, and it logs mounted from an effect that runs on mount. For now, take “runs once, when the component mounts” as given; effects are a later chapter. The parent re-renders itself exactly three times (it bumps n until it hits 2). Predict what prints.
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 in the wild: if a component you expected to mount once is mounting again and again, suspect an unstable key feeding it a new identity every render.
The second edge is resetting state the user wanted to keep. A key change is a blunt instrument: it discards all local state in the subtree it wraps, indiscriminately. If that subtree also holds a scroll position, an expanded panel the user opened, or a sub-selection they were in the middle of, the key change throws those away too. Be deliberate about both what the key wraps and when it changes. If only part of the subtree should reset, key only that part. This is the same “key the smallest owning subtree” rule from a moment ago, now as a guard against destroying state the user expected to survive.
One last practical note. A remounted child always starts from useState’s initial value, so if the fresh instance should come up pre-filled, feed it through props and initialize from them with useState(user.email), which is exactly the record-bound-form shape you started with. (If that initial value is expensive to compute, there is a lazy form, useState(() => …), that you will meet in the next chapter, named here only so you know it exists.)
Where to go deeper
Section titled “Where to go deeper”Two pages of the React docs cover this material directly, and a third piece builds the intuition behind it. Together they make the best follow-up reading.
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.
You now have the whole tool. A key is identity, and identity is something you choose: keep it stable and React reuses the instance, change it and React hands you a fresh one. So when a stateful component’s identity changes, whether through a new record, a new message, or a deliberate reset, change its key and let the remount do the work. Just reach for it last, after you have ruled out persisting, deriving, and lifting, and only when the state has a real home in the child whose identity the parent is already tracking.