Engineering Diaries · React
§
ReactReconciliationFrontend Internals

React's key Prop Is A Reset Button, Not An Identity Prop. Here Is What Most Engineers Get Wrong.

Every React tutorial teaches keys the same way - 'they help React tell items apart in a list'. That framing is why production React code is full of stale state, ghost animations, and forms that remember the last user's input. key is a reset primitive, not an identity marker.

01 · The Bug That Started This

The Ghost State That Would Not Go Away

A while back on a payroll app at scale I inherited a page that did something haunting. Users would open the employee-detail drawer, edit two fields, close it, click on a different employee, and see the previous employee's edits sitting in the input boxes. Not from the database - visually, in the drawer, the old text was still there.

The team had already tried the usual patches. They had added a useEffect that called setLocalDraft(null) when the employeeId prop changed. It missed some edge cases. Someone added a second effect for a related bit of state. That one also missed. Someone tried a forceUpdate. It briefly appeared to work, then broke a scroll-position thing three sprints later.

The eventual fix was one line. Not in the drawer. In the parent.

the actual fix - one line, in the parent
<EmployeeDrawer key={selectedEmployeeId} employeeId={selectedEmployeeId} />

That was it. Every reset effect got deleted. The forceUpdate got deleted. The scroll bug from the earlier fix went away because the earlier fix went away. And the page started behaving like a page that had been designed by people who knew what they were doing.

What that one line did is what most engineers I have worked with think keys are for lists, and only for lists. Once you see it, you cannot unsee it.

"key is not an identifier. It is an instruction: when this value changes, throw the old component instance away and mount a fresh one."

02 · What The Tutorials Say

The Framing That Every Beginner Article Uses (And Why It Misleads)

If you learned React in the last five years, the framing you got for keys was almost certainly this: "When you render a list, add a unique key so React can tell the items apart." That framing is not wrong. It is just so incomplete that it leads to a wrong mental model.

The wrong mental model is that key is a kind of ID. That React looks at keys the way a database looks at primary keys - "which row is this". If you hold that model, you never think to use a key outside of a list, because outside of a list there is no ambiguity. There is only one drawer. Why would it need an identifier?

The actual role of key is different. It is a hint to React's reconciler about whether two renders of the same JSX position refer to the same component instance or a different one. If the key changes, React does not reuse the instance - it throws it away and mounts a new one from scratch. All state resets. All effects re-fire. All refs re-attach.

The list use-case is a special case of this general rule. In a list, React needs to decide which of the previously-rendered items each new item corresponds to, and keys are the tie-breaker. But that is one application. Keys work outside of lists too, and outside of lists they are less about identity and more about deliberate resets.

03 · What key Actually Does

The Reconciler's Rule In One Paragraph

The precise rule, from React's own reconciliation docs, is this: when React re-renders a parent, at each position in the JSX tree it compares the previous element to the new element. If the element type is the same and (in a keyed position) the key is the same, React reuses the existing component instance and calls its update path. If either the element type changes or the key changes, React unmounts the previous instance and mounts a new one.

So the two things that make React "reset" a subtree are:

  • The element type changes. <A /> becomes <B />. Old instance dies, new one mounts. This is why swapping <input /> for <textarea /> loses the typed value.
  • The key changes. Same element type, different key. Also: old instance dies, new one mounts. Same effect on state.

That is the whole rule. Everything key-related in the framework flows from those two lines.

The one-sentence definition key is a stable label attached to a JSX position. When the label changes, React treats the child at that position as a different component - unmount old, mount new. When the label stays the same, React reuses the instance across renders.

Which is why calling it an "identity prop" is misleading. It is not the identity of the item. It is the identity of the instance. And you get to choose when that identity should change.

04 · The Reset Trick In One Example

How The Drawer Actually Got Fixed

Back to the drawer story. Here is the "before" version, stripped down. The drawer keeps a local draft of the edits, plus effects that reset it whenever the target employee changes.

before - state resets by hand
function EmployeeDrawer({ employeeId }) {
  const [draft, setDraft] = useState(null);
  const [tab, setTab] = useState('details');

  useEffect(() => {
    setDraft(null);
    setTab('details');
    // don't forget the scroll position, the search filter, the pinned tab...
  }, [employeeId]);

  // ...
}

Every field of local state has to be listed in that effect. Miss one - and this is what happened in the ghost-state bug - and it silently persists across employees. The bug is not a mystery once you find it. The problem is that every new field of state added to the drawer is a chance to miss the reset.

Now here is the "after". The drawer keeps its local state exactly as it was. But the parent hands it a key.

after - the parent tells React to reset the subtree
function EmployeePage() {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <>
      <EmployeeList onSelect={setSelectedId} />
      <EmployeeDrawer key={selectedId} employeeId={selectedId} />
    </>
  );
}

The key is on the outside of the component boundary. When selectedId changes, the entire EmployeeDrawer instance is thrown away and a new one is mounted. The draft, the tab, the scroll position, the pinned filter, and any field of state we might add later - all of it starts fresh. Because it is a new instance.

The drawer's own code did not have to change. It did not have to know that a reset is happening. And every future engineer who adds a new piece of local state to that drawer gets the reset for free.

"The right layer to solve 'reset when this identifier changes' is the parent. State that a parent controls should be controlled with a key, not with reset effects inside the child."

05 · Where Else This Applies

Five Patterns Where key Replaces Whole Categories Of Code

The drawer is one example. The general shape - a subtree that should reset when some external identifier changes - shows up in a lot of places. Once you spot the pattern, you find it every couple of files.

1. Modal that shouldn't remember the last dialog

A confirmation modal that opens for different actions. If it uses defaultValue on its inputs, the previous action's typed text leaks through. Key on the action id, not on "isOpen".

2. Route-based form pages

A profile page and a settings page that both mount the same generic <Form>. Without a key, navigating between them can reuse the same form instance, and any uncontrolled inputs carry state across the routes. Key on the route or the form id.

3. Charts that need to re-run enter animations

A d3 or Recharts chart that runs an entry animation on mount. When the dataset changes to a different scenario, you want the animation to play again. Instead of imperatively rebuilding the chart, key on the scenario id.

4. Infinite scrolls after filter changes

An infinite list with pagination state, cache, and scroll position. When the filter changes, all that internal state is wrong for the new filter. Reset effects grow. A key on the filter signature resets everything.

5. Editors bound to a document

A rich-text editor with undo history bound to a document id. When the document changes, the old undo history is meaningless for the new one. Key on the document id and the editor mounts fresh.

In every one of those cases, the alternative is a growing pile of "reset X when Y changes" effects inside the child. Every new field of state means another line in the reset list. The key approach is one line at the parent, and every new field gets reset automatically because a fresh instance has no memory of the old one.

06 · The Cost To Be Honest About

What Changes When You Force A Remount

This is not a free trick. Changing a key throws away the instance, and that has real consequences worth naming.

  • All effects re-fire. Any useEffect in the subtree runs its cleanup and then its setup again. If you have expensive setup - opening a WebSocket, warming a cache, requesting a permission - it will pay the cost every time the key changes.
  • All refs re-attach. DOM refs are re-established. If you have imperative code that stored a handle to the old DOM node, it is stale.
  • Focus is lost. A new DOM element means the previous focus is gone. If the reset happens while the user is typing, that is a jarring UX.
  • Enter animations replay. Anything animated on mount plays again. Usually a feature; sometimes surprising.
  • Suspense boundaries can re-suspend. A fresh instance that reads from a resource may re-suspend, showing a fallback again.

The rule of thumb after living with this pattern for a while: use key resets for state that should reset with the identifier. If the state is genuinely tied to the identifier - draft edits for an employee, undo history for a document, animation state for a scenario - a remount is exactly what you want. If the state is more like a global preference or a cross-cutting cache, do not attach it to a key-driven subtree.

The Guardrail I Use In Reviews

When someone writes a useEffect whose entire body is calling setState back to defaults because a prop changed, that is almost always a signal that the parent should be using key. If the reset logic exists for correctness rather than for a side-effect, it belongs at the parent as a key, not inside the child as an effect.

07 · The Anti-Patterns

Two Ways To Use key That Go Very Wrong

The reset power of key cuts both ways. Two anti-patterns show up often enough to name.

1. Key on Math.random or a per-render value

what NOT to do
<MyThing key={Math.random()} />
<MyThing key={Date.now()} />

Every render produces a new key, so the instance is thrown away every single render. All state resets on every re-render. All effects re-fire. This is essentially forceUpdate-on-steroids and it will destroy performance and correctness in equal measure. If you see this in a codebase, someone was fighting a symptom - almost always with the wrong tool - and never came back to fix it.

2. Using array index as key in a list that reorders or filters

what LOOKS fine but is a bug factory
{items.map((item, i) => <Row key={i} item={item} />)}

If the list order can change, this is a bug. React thinks position 0 is still the same component after items are re-sorted, so it keeps the previous instance and just updates its props. Any local state on the row - a form draft, an expand/collapse toggle - stays attached to whatever item happens to be at index 0 now, which is not the item it was attached to before. This is the archetypal "why did my checkbox state jump to the wrong row after I deleted an item" bug, and it always traces back to index-as-key.

The rule: key must be stable across time for the same logical item, and unique across items in the same list. Anything that violates that is almost certainly wrong.

08 · The Mental Model To Keep

Two Simple Rules That Cover Almost Every Case

After a few years of writing React with keys used deliberately, I have settled on two rules that cover almost every real-world case.

  • In a list, key must be stable and unique per item. Not index. Not Math.random. The item's own ID or a hash of its content. If the item does not have a natural ID, you are usually one refactor away from being able to give it one.
  • Outside a list, key is your reset button. Use it whenever a subtree's identity is tied to an external value. Change the external value, get a fresh instance for free. Do not chase resets from inside the child.

Two lines. That is the whole thing.

1
rule for lists
1
rule for non-lists
many
reset effects deleted
0
Math.random keys

Every advanced pattern - controlled resets, deliberate remounts, animation replays, undo-history clearing, uncontrolled input reset on route change - falls out of those two rules. You do not need a library. You do not need a hook. You need to think of key as a knob you can turn.

09 · The Broader Lesson

Reset Primitives Beat Reset Choreography

Zooming out a bit, this is one instance of a broader principle in frontend engineering: reset primitives beat reset choreography. The choreography approach says "when X changes, I have to remember to reset A, B, C, D, and E". Every new field of state is a new thing to remember. Every new field is a new bug waiting to happen.

The primitive approach says "when X changes, the whole thing is a new instance". You do not remember to reset A through E. There is no A through E - there is a fresh instance whose state started as its initial state, because it is a fresh instance. This is the same reason immutable data structures beat mutation-plus-invalidation, why derived state beats synchronised state, and why Suspense beats manual loading states.

React's key prop is one of the cleanest examples of this principle available anywhere in frontend. It is a single-character contract - the letter k, plus an equals sign - that lets you say "throw this instance away when this value changes". Once you internalise that, code that used to be full of imperative reset dance becomes a declarative statement about identity.

"The best React feature that most codebases underuse is not a hook and it is not a component. It is a five-letter attribute you were told was for lists."

10 · The Practical Ask

What To Actually Do Next

Two concrete things you can do this week if you write React for a living.

  • Grep your codebase for useEffect calls whose body is only setState calls back to initial values. Every one of those is a candidate for being deleted in favour of a key on the parent. Some will legitimately have to stay - a reset that is more than a state-reset, say cancelling an in-flight request - but a lot of them will just disappear.
  • Read the "Preserving and resetting state" page in the React docs. It is one of the most useful pages in the entire documentation set, and it is one that most engineers I have talked to have never opened. Everything in this article is on that page in one form or another; it just does not get taught.

The next time a state-carrying component is behaving as if it remembers something it should not remember, resist the urge to add a reset effect. Ask if the parent has an identifier that could go on a key instead. It usually does.

Written by Divyansh Singh - senior frontend engineer, ex-fintech, currently on frontend infrastructure at a large HR / payroll platform. Author of Frontend System Design Decoded. This piece grew out of a real bug from a payroll app at scale where a drawer would leak the previous employee's edits, and the fix turned out to be a single key attribute on the parent. The underlying rule is straight from the React reconciler and the "Preserving and resetting state" section of the official docs.