Skip to content
React rc rendering 5 min read

Keys In Depth

A key is React’s answer to a deceptively hard question: when a list re-renders, which old element corresponds to which new one? React cannot read your mind, so it uses the key prop as a stable identity tag for each item. Get keys right and component state, focus, and DOM nodes follow your data faithfully through reorders, insertions, and deletions. Get them wrong — most commonly by using the array index — and React silently attaches the wrong state to the wrong row.

How React matches old elements to new

During reconciliation React compares the previous tree against the next one. For a list of siblings, it pairs children by their key (within the same position in the parent). If a key exists in both renders, React treats it as the same component instance: it reuses the existing DOM node and preserves that component’s state and effects. If a key disappears, the instance unmounts; if a new key appears, a fresh instance mounts.

Keys only need to be unique among siblings, not globally. They are never passed to your component as a prop — props.key is undefined. They exist purely for React’s bookkeeping.

function ContactList({ contacts }) {
  return (
    <ul>
      {contacts.map((contact) => (
        <li key={contact.id}>{contact.name}</li>
      ))}
    </ul>
  );
}

Because contact.id is stable and tied to the data, reordering contacts reorders the DOM nodes without recreating them. React moves the existing <li> rather than tearing it down and rebuilding it.

Tip: A good key is stable, unique, and derived from the data itself — a database id, a UUID, a slug. Never generate keys during render with Math.random() or crypto.randomUUID(): a brand-new key on every render forces React to remount the item every time, destroying its state and trashing performance.

Why index keys break on reorder and insert

key={index} is the classic trap. It looks fine until the list changes shape, because the index describes a position, not an item. When you insert at the front or reorder, the item at index 0 is now a different piece of data — but React sees the same key and reuses the previous instance’s state for it.

The bug is most visible with uncontrolled state inside each row.

import { useState } from "react";

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

function Demo() {
  const [items, setItems] = useState(["Alpha", "Bravo", "Charlie"]);

  function addToFront() {
    setItems(["Zulu", ...items]);
  }

  return (
    <div>
      <button onClick={addToFront}>Add to front</button>
      <ul>
        {items.map((label, index) => (
          // BUG: index keys
          <Row key={index} label={label} />
        ))}
      </ul>
    </div>
  );
}

Check the box next to “Alpha”, then click Add to front. You expect the checkmark to travel with “Alpha”. Instead it stays on the first row, which is now “Zulu”.

Output:

Before:  [ ] Zulu   <-- (not yet added)
         [x] Alpha  <-- you checked this one

After:   [x] Zulu   <-- checkmark stuck to index 0
         [ ] Alpha  <-- lost its state
         [ ] Bravo
         [ ] Charlie

React kept the instance whose key was 0 and simply changed its label prop from “Alpha” to “Zulu”. The component state (checked) belonged to position 0, not to “Alpha”. Swap key={index} for key={label} (or a real id) and the checkmark follows the data correctly.

Index keys are only safe when the list is static — never reordered, filtered, inserted into, or deleted from — and the items have no internal state. When in doubt, use a real id.

Resetting component state by changing the key

The same mechanism that preserves state can be used deliberately to reset it. Because React remounts a component when its key changes, giving a component a key tied to “what it represents” forces a clean slate whenever that identity changes.

A common case is a form that should reset its fields when you switch the entity it edits.

import { useState } from "react";

function ProfileForm({ userId }) {
  const [draft, setDraft] = useState("");
  return (
    <input
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
      placeholder={`Notes for user ${userId}`}
    />
  );
}

function Editor({ userId }) {
  // Changing userId changes the key -> ProfileForm remounts with empty draft.
  return <ProfileForm key={userId} userId={userId} />;
}

Without the key, navigating from user 42 to user 99 would leave the half-typed draft in the input. With key={userId}, switching users mounts a fresh ProfileForm, clearing draft automatically — no useEffect cleanup required. This is the idiomatic way to reset state on identity change.

Keys for fragments and array siblings

When you render multiple elements per item, you may need a fragment wrapper that itself carries a key. The shorthand <>...</> cannot take a key, so use the explicit <Fragment> form.

import { Fragment } from "react";

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((term) => (
        <Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Each Fragment is keyed even though it renders no DOM node of its own, so React can match the <dt>/<dd> pairs across renders. Any time .map() returns elements, every top-level returned element needs a key — including fragments.

Key behavior reference

SituationWhat React does
Same key in both rendersReuses instance; preserves state, refs, effects
Key absent in new renderUnmounts the instance
New key in new renderMounts a fresh instance
Key changes for a positionUnmounts old, mounts new (state reset)
Duplicate keys among siblingsUndefined matching; React warns in dev
Missing key in a listFalls back to index; dev warning logged

Best Practices

  • Use a stable id from your data as the key; reach for the array index only when the list is static and items are stateless.
  • Never produce keys with Math.random() or crypto.randomUUID() during render — they remount every item on every render.
  • Keep keys unique among siblings only; they do not need to be globally unique and are not readable as props.
  • Use key={entityId} on a component to intentionally reset its state when the entity it represents changes.
  • Use the explicit <Fragment key={...}> form when mapping to multiple sibling elements; the <> shorthand cannot hold a key.
  • Treat React’s “each child should have a unique key” warning as a real bug, not noise — it predicts state corruption on reorder.
Last updated June 14, 2026
Was this helpful?