ESC

Type to search the knowledge base.

Keys in Lists

Why React keys exist — identity across reconciliations, index key bugs, remounting with key, and how to choose stable ids.

beginner6 min read
  • react
  • keys
  • lists
  • reconciliation

React’s list API looks like “map to elements.” The hard part is identity: when the array changes, which old fiber matches which new element? That match is driven by key among sibling elements of the same parent.

Keys are not a performance prop you sprinkle for good luck. They are how reconciliation decides update vs remount for list children. Wrong keys produce glitched inputs, state that teleports between rows, and “impossible” UI bugs.

Core model of the tree: Reconciliation & the Virtual DOM.

The problem keys solve

function TodoList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li>
          <input defaultValue={item.text} />
        </li>
      ))}
    </ul>
  );
}

Without keys, React falls back to index among siblings. Insert at the front, reorder, or delete in the middle, and components reuse the wrong DOM nodes and local state. With a stable item.id, each row keeps its own fiber.

{items.map((item) => (
  <li key={item.id}>
    <input defaultValue={item.text} />
  </li>
))}

Model: type + key + position

On the same parent, React matches children roughly by:

  1. key (if present)
  2. type (div vs Row vs span)
  3. position heuristics when keys are missing

Same type + same key → update that fiber (props change, state preserved).
Different type or intentional key change → unmount old, mount new (state resets).

// Reorder with stable keys — state follows the item id
const items = [
  { id: 'a', text: 'One' },
  { id: 'b', text: 'Two' },
];

Swap a and b in the array: the fiber for a moves with a. Index keys would keep “position 0’s state” on the new first row — which might now be b.

Index as key — when it bites

// Fragile when list mutates mid-array
{items.map((item, index) => (
  <Row key={index} item={item} />
))}
List change Index keys Stable id keys
Append only Often OK OK
Prepend / insert middle State shifts to wrong rows State stays with id
Reorder / sort State sticks to positions State follows items
Delete middle Later rows inherit earlier state Correct unmount

Static lists that never reorder (e.g. fixed two tabs with constant order) can use index without drama. As soon as the list is user-editable, index keys are a bug farm.

Local state is tied to the fiber, not your data row

function Row({ item }) {
  const [checked, setChecked] = useState(false);
  return (
    <label>
      <input
        type="checkbox"
        checked={checked}
        onChange={() => setChecked((c) => !c)}
      />
      {item.label}
    </label>
  );
}

function List({ items }) {
  return items.map((item, i) => <Row key={i} item={item} />);
}

Check row 0, then prepend an item with index keys: the checked box appears on the new row 0. With key={item.id}, the checked state stays on the original item’s fiber.

If the checked flag is product state, store it in data (or a parent map keyed by id) — don’t rely on anonymous local state for source of truth that must survive list edits.

Keys must be stable, unique among siblings

Rules that hold in code review:

  • Stable — same item → same key across renders.
  • Unique among siblings — not globally unique across the whole app, but no duplicates under one parent.
  • Do not use random keys (key={Math.random()}) — remounts every render.
  • Do not use array index when order/length changes.
  • Prefer business ids from the server or a client-generated id at creation time.
// Bad: new key every render
<Row key={Math.random()} />

// Bad: key derived from changing display text only
<Row key={item.label} /> // labels collide; renames remount

// Good
<Row key={item.id} />

Using key to reset state on purpose

Changing key is a supported way to remount a subtree when identity should start fresh:

function UserEditor({ userId }) {
  // Changing userId remounts the form — no stale fields from previous user
  return <ProfileForm key={userId} userId={userId} />;
}

Same idea for wizard steps, selected conversation threads, or “create new” vs “edit id”. This is deliberate identity change, not a list bug. See also resetting patterns under related articles like Keys and Remounting State when present.

Fragments and keys

When a map must return multiple siblings, key the Fragment:

import { Fragment } from 'react';

{items.map((item) => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.description}</dd>
  </Fragment>
))}

Short syntax <>...</> cannot take a key. Use <Fragment key={...}>.

Keys are not props

function Row({ id }) {
  // `key` is NOT available here as props.key
  return <div data-id={id} />;
}

// Parent:
<Row key={item.id} id={item.id} />

React consumes key (and ref) specially. Pass id explicitly if the child needs the identifier.

Nested lists

Keys only need uniqueness among siblings. Nested lists can reuse the same key strings under different parents:

sections.map((section) => (
  <section key={section.id}>
    {section.rows.map((row) => (
      <Row key={row.id} row={row} />
    ))}
  </section>
))}

Still avoid index at every level if those arrays mutate.

Animation libraries and keys

Enter/exit animations often key by id so the reconciler and FLIP libraries agree on identity. Changing a key mid-animation remounts — which can be what you want (replay) or a flicker (accidental).

Footguns

  1. Index keys + controlled inputs — classic “typing jumps between rows.”
  2. Using index and claiming “we don’t reorder” until product adds sort six months later.
  3. Composite keys from unstable data (key={item.updatedAt}).
  4. Forcing remount with random keys to “fix” state bugs — hides the real state ownership problem.
  5. Duplicate keys — React warns; matching becomes undefined territory.
  6. Putting key on the wrong element — key must be on the outermost element returned from the map callback.

Interview angle

Prompt: “Why do we need keys in lists?”

Strong answer: “Reconciliation matches list children across renders. Keys give stable identity so React can move/update the correct fiber and preserve state. Index keys couple identity to position, so inserts and reorders attach state to the wrong items. Keys should be stable and unique among siblings; changing a key intentionally remounts and resets state.”

Follow-ups: difference between re-render and remount; how keys interact with memo; why virtualization still needs stable ids.

Further reading

Related guides