| |

React 24 ⚛️ The Importance of Keys in Lists

The previous chapter introduced map as the way to render lists in React and mentioned the key prop as a requirement. This chapter is about why. The key prop is the single most misunderstood part of list rendering. It looks like a prop, behaves unlike any other prop, and produces bugs that are invisible until the list changes order. Developers who treat keys as boilerplate—adding key={index} to silence the warning—write code that works until it does not, and then struggle to understand why.

This chapter is dedicated entirely to the key prop. You will learn what React’s reconciliation algorithm actually does with keys, what happens when they are missing, unstable, or duplicated. You will see the index key trap in its full detail, including a concrete example where input values end up attached to the wrong items. You will learn the rules for choosing a good key and the situations where the index is genuinely acceptable. And you will see how keys affect performance, not just correctness.

By the end, you will understand keys as a first-class concept in React’s rendering model, not as a lint rule to satisfy. You will know how to choose a key deliberately and how to recognize when a key choice is creating subtle bugs.

Key point: React uses the key prop to match elements between renders. When keys are stable and unique, React preserves DOM nodes and component state for items that remain in the list. When keys are unstable or duplicated, React’s matching breaks down, and state attaches to the wrong elements.


Why keys matter at all

The reconciliation problem. React re-renders by comparing the new tree of elements against the previous one and computing the minimum set of changes. For a single element, this comparison is simple: same type, update props; different type, replace. For a list, the comparison is harder. React must decide which old element corresponds to which new element. The key prop provides that correspondence. Without keys, React uses position as the implicit correspondence, which works for static lists and fails for anything that changes.

The state problem. React preserves component state across renders when it can identify a component instance. A list of components with stable keys keeps each component’s state attached to its item. A list with index keys or no keys keeps state attached to positions. When the list reorders, the state does not follow the item; it stays where it was. Form inputs show the wrong values, animations glitch, and local state becomes inconsistent with the data it represents.

The performance problem. Keys affect how many DOM operations React performs. With stable keys, React moves existing DOM nodes when the order changes. Without them, React patches content in place, which may be fewer operations for a simple list but more operations for a list with stateful or complex items. The performance difference is usually secondary to the correctness issue, but it matters for large or frequently updated lists.

The duplication problem. Keys must be unique among siblings. Duplicate keys confuse React’s matching algorithm and produce unpredictable results. Two elements with the same key may both be rendered, one may be dropped, or state may be shared between them. The React docs are explicit: keys must be unique among siblings, and React does not guarantee behavior with duplicates.

The React philosophy. React’s reconciliation algorithm is an implementation detail, but the key prop is its public interface. Keys let you tell React what you already know: which items in the list are the same between renders. This is why keys are not passed to components—they are not data, they are instructions for the reconciler.


a. What React does with keys

When React renders a list, it iterates the new array of elements and compares each one against the previous array. For each new element, it looks for an old element with the same key and type. If it finds one, React updates that element rather than creating a new one. If it does not find one, React creates a new element. Old elements with no matching new element are removed.

This algorithm has a few important consequences:

  • Keys are scoped to siblings. Two lists in different parts of the tree can use the same keys without conflict. React only compares keys within the same parent.
  • Keys are not part of props. React extracts the key prop before passing props to the component. A component cannot read props.key.
  • Keys must be strings or numbers. React converts them to strings internally. Objects and arrays are not valid keys and produce a warning.
  • Keys do not need to be globally unique. They need to be unique within their list. A user ID can appear as a key in multiple lists without issue.

The comparison happens for every render. If the list has not changed, the keys match, and React reuses all existing elements. If items have been added, React creates new elements for the new keys. If items have been removed, React removes the elements with those keys. If items have been reordered, React moves the existing elements to match the new order.


b. The index key trap in detail

Using the array index as a key is the most common mistake. It seems harmless because the index is unique within the list and always present. But the index is a property of the position, not of the item. When the list changes, the index that corresponded to an item may now correspond to a different item.

Consider a list of todos with inputs:

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input defaultValue={todo.text} />
        </li>
      ))}
    </ul>
  );
}

Suppose the initial todos array is [{id: 1, text: 'Buy milk'}, {id: 2, text: 'Walk dog'}]. The rendered list has two items with keys 0 and 1. The user types into the second input, changing its DOM value to something. The React state is unchanged—the input uses defaultValue, so it is uncontrolled—but the DOM input now holds the typed value.

Now the list is reordered: [{id: 2, text: 'Walk dog'}, {id: 1, text: 'Buy milk'}]. The keys are still 0 and 1. React sees the same keys and updates the content in place. The first <li> now shows “Walk dog” but its input still holds whatever the user typed into the old second input. The second <li> shows “Buy milk” but its input holds the old first input’s value. The state has not moved with the items. This is the bug.

The fix is to use a stable key:

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

Now the keys are 1 and 2. When the list reorders, React sees the keys in a different order and moves the corresponding DOM nodes. The input that belonged to “Walk dog” moves with “Walk dog,” and the typed value stays where it should.

The index is not always wrong. For a static list that never reorders, filters, or changes length, the index is stable, and using it causes no bug. But static lists are rare, and the cost of using a stable key is small. The safe default is to use a stable ID whenever one exists.


c. Choosing a good key

A good key is stable, unique among siblings, and derived from the data. It should not change between renders unless the item it identifies has changed.

Database IDs are the ideal key. They are unique, stable, and already part of the data.

{users.map((user) => <li key={user.id}>{user.name}</li>)}

Composite keys work when no single field is unique. Combine fields with a separator to produce a unique string.

{items.map((item) => (
  <li key={`${item.type}-${item.id}`}>{item.name}</li>
))}

The separator matters. Without it, type: 'ab', id: 'c' and type: 'a', id: 'bc' both produce 'abc', which is a collision. A character that cannot appear in either field—a colon, a pipe, a slash—prevents this.

Content strings work when the content is unique and stable. A list of category names can use the name as a key if names are guaranteed unique and do not change.

{categories.map((category) => (
  <li key={category.name}>{category.name}</li>
))}

This breaks if a category is renamed, because React sees a new key and recreates the element. If renaming is possible, use an ID instead.

Index keys are acceptable only when the list is static: it never reorders, never filters, and never changes length. A list of days of the week, a fixed set of menu items, a grid of cells that only updates in place. Even then, a stable key is usually available and preferable.

Random keys are never acceptable. A key that changes on every render tells React that every element is new, so React destroys and recreates the entire list on every render. This is both a correctness problem (state is lost) and a performance problem (the DOM is rebuilt).

What not to use as a key:

  • The array index for dynamic lists.
  • Math.random() or any value that changes between renders.
  • Object or array values.
  • The item itself when it is not a primitive.
  • Duplicated values from the data.

Complete Example Session

// ============================================
// PART 1: STABLE ID KEY (CORRECT)
// ============================================
function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
// ============================================
// PART 2: INDEX KEY WITH INPUTS (BUGGY)
// ============================================
function BuggyList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          <input defaultValue={item.text} />
          <span>{item.text}</span>
        </li>
      ))}
    </ul>
  );
}
// Reordering leaves input values in old positions
// ============================================
// PART 3: STABLE KEY WITH INPUTS (FIXED)
// ============================================
function FixedList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <input defaultValue={item.text} />
          <span>{item.text}</span>
        </li>
      ))}
    </ul>
  );
}
// Inputs move with their items
// ============================================
// PART 4: DEMONSTRATING THE REORDER BUG
// ============================================
function ReorderDemo() {
  const [items, setItems] = React.useState([
    { id: 1, text: 'First' },
    { id: 2, text: 'Second' },
    { id: 3, text: 'Third' }
  ]);

  const shuffle = () => {
    setItems([...items].sort(() => Math.random() - 0.5));
  };

  return (
    <div>
      <button onClick={shuffle}>Shuffle</button>
      <BuggyList items={items} />
      <FixedList items={items} />
    </div>
  );
}
// ============================================
// PART 5: COMPOSITE KEY
// ============================================
function EventList({ events }) {
  return (
    <ul>
      {events.map((event) => (
        <li key={`${event.date}-${event.id}`}>
          {event.date}: {event.title}
        </li>
      ))}
    </ul>
  );
}
// ============================================
// PART 6: CONTENT STRING KEY
// ============================================
function CategoryList({ categories }) {
  return (
    <ul>
      {categories.map((name) => (
        <li key={name}>{name}</li>
      ))}
    </ul>
  );
}
// Safe only if names are unique and never change
// ============================================
// PART 7: INDEX KEY FOR STATIC LIST (ACCEPTABLE)
// ============================================
function WeekdayList() {
  const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  return (
    <ul>
      {days.map((day, index) => (
        <li key={index}>{day}</li>
      ))}
    </ul>
  );
}
// Static list, never reorders, index is stable
// ============================================
// PART 8: RANDOM KEY (BROKEN)
// ============================================
function BrokenList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={Math.random()}>{item.text}</li>
      ))}
    </ul>
  );
}
// New key every render: entire list is recreated
// ============================================
// PART 9: DUPLICATE KEYS (BROKEN)
// ============================================
function DuplicateKeyList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key="same">{item.text}</li>
      ))}
    </ul>
  );
}
// All items share a key: React cannot distinguish them
// ============================================
// PART 10: KEY DOES NOT PASS TO COMPONENT
// ============================================
function Item({ id, text }) {
  return <li>{id}: {text}</li>;
}

function Parent({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <Item key={item.id} id={item.id} text={item.text} />
      ))}
    </ul>
  );
}
// Item receives id and text, but not key

The ten parts covered stable keys, the index key bug with inputs, the fix, a working demonstration, composite keys, content string keys, static index keys, random keys, duplicate keys, and the fact that key is not passed to components.


Quick Reference

Key Rules

RuleRequirement
Unique among siblingsYes
Stable across rendersYes
TypeString or number
Passed to componentNo
ScopeSiblings only
Required for listsYes

Key Quality

Key SourceStabilityRecommendation
Database IDStableBest choice
Composite fieldsStableGood when no single ID
Content stringStable if content is immutableAcceptable
Array indexUnstable on reorderOnly for static lists
Random valueUnstable every renderNever
Object valueInvalidNever

What Happens With Each Key Choice

Key ChoiceReorder BehaviorState BehaviorPerformance
Stable IDElements moveState follows itemOptimal
IndexContent patchesState stays in positionSuboptimal
MissingContent patchesState stays in positionSlowest
RandomFull recreateState lostWorst
DuplicateUndefinedShared or lostUnpredictable

Index Key Acceptability

SituationIndex OK?
Static listYes
Never reordersYes
Never filtersYes
Never adds/removesYes
Append-onlySometimes
ReordersNo
FiltersNo
Has stateful itemsNo

Best Practices

✅ Do This:

// Use database IDs
{users.map((user) => <li key={user.id}>{user.name}</li>)}    // ✅

// Use composite keys when needed
{items.map((i) => <li key={`${i.type}-${i.id}`}>{i.name}</li>)} // ✅

// Use a stable content key when content is immutable
{tags.map((tag) => <span key={tag}>{tag}</span>)}            // ✅

// Pass the ID as a separate prop if the component needs it
<Item key={item.id} id={item.id} data={item} />              // ✅

// Use index only for genuinely static lists
{days.map((day, i) => <li key={i}>{day}</li>)}               // ✅ (static)

// Test with reordering to catch key bugs
setItems([...items].reverse());                              // ✅

❌ Don’t Do This:

// Use index for dynamic lists
{items.map((item, i) => <li key={i}>{item.text}</li>)}       // ❌

// Use random values as keys
{items.map((item) => <li key={Math.random()}>{item.text}</li>)} // ❌

// Use duplicate keys
{items.map((item) => <li key="x">{item.text}</li>)}          // ❌

// Use object values as keys
{items.map((item) => <li key={item}>{item.text}</li>)}       // ❌

// Try to read key inside the component
function Item(props) {
  return <li>{props.key}</li>;                               // ❌
}

// Omit keys entirely
{items.map((item) => <li>{item.text}</li>)}                  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Input values mismatch after reorderIndex used as keyUse stable ID
Entire list recreates on renderRandom keyUse stable identifier
State shared between itemsDuplicate keysEnsure keys are unique
Key warning in consoleKey omittedAdd key prop
props.key is undefinedKey not passed to componentPass ID as separate prop
Content updates in wrong orderIndex key with reorderUse stable ID
Animation restartsUnstable keyUse stable identifier

Real-World Examples

1. Todo List with Inputs

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

2. Sortable Table

{sortedUsers.map((user) => (
  <tr key={user.id}>
    <td>{user.name}</td>
  </tr>
))}

3. Filtered Search Results

{results
  .filter((r) => r.match)
  .map((r) => <Result key={r.id} data={r} />)}

4. Composite Key for Join Table

{userRoles.map((ur) => (
  <li key={`${ur.userId}-${ur.roleId}`}>
    {ur.userId} → {ur.roleId}
  </li>
))}

5. Static Weekday List

{['Mon', 'Tue', 'Wed', 'Thu', 'Fri'].map((day, i) => (
  <li key={i}>{day}</li>
))}

6. Passing ID Separately

{items.map((item) => (
  <Item key={item.id} id={item.id} data={item} />
))}

7. Nested Lists with Keys

{categories.map((cat) => (
  <div key={cat.id}>
    {cat.items.map((item) => (
      <li key={item.id}>{item.name}</li>
    ))}
  </div>
))}

8. Reordering Test

const shuffle = () => setItems([...items].reverse());

9. Duplicate Detection

const keys = items.map((i) => i.id);
const hasDuplicates = new Set(keys).size !== keys.length;

10. Key with Template Literal

{events.map((e) => (
  <li key={`event-${e.id}`}>{e.title}</li>
))}

Visual

Key Matching in Reconciliation

┌─────────────────────────────────────────────────────────────┐
│  REACT MATCHES ELEMENTS BY KEY                              │
│                                                             │
│  Previous render:        New render:                        │
│  ┌─────────────┐         ┌─────────────┐                    │
│  │ key: 1      │         │ key: 3      │                    │
│  │ key: 2      │         │ key: 1      │                    │
│  │ key: 3      │         │ key: 2      │                    │
│  └─────────────┘         └─────────────┘                    │
│                                                             │
│  React matches:                                             │
│    key 1: old pos 1 → new pos 2  (MOVE)                     │
│    key 2: old pos 2 → new pos 3  (MOVE)                     │
│    key 3: old pos 3 → new pos 1  (MOVE)                     │
│                                                             │
│  DOM nodes move, state preserved with each item.            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Index Key Failure

┌─────────────────────────────────────────────────────────────┐
│  INDEX KEY: STATE STAYS IN POSITION                         │
│                                                             │
│  Initial:                                                   │
│  ┌───────────────────────────────────────────────────┐      │
│  │ key 0: [input: "Buy milk"]   label: "Buy milk"    │      │
│  │ key 1: [input: "Walk dog"]   label: "Walk dog"    │      │
│  └───────────────────────────────────────────────────┘      │
│                                                             │
│  User types "X" into second input.                          │
│  ┌───────────────────────────────────────────────────┐      │
│  │ key 0: [input: "Buy milk"]   label: "Buy milk"    │      │
│  │ key 1: [input: "X"]          label: "Walk dog"    │      │
│  └───────────────────────────────────────────────────┘      │
│                                                             │
│  After reorder (labels swap):                               │
│  ┌───────────────────────────────────────────────────┐      │
│  │ key 0: [input: "Buy milk"]   label: "Walk dog"    │      │
│  │ key 1: [input: "X"]          label: "Buy milk"    │      │
│  └───────────────────────────────────────────────────┘      │
│                                                             │
│  Input "X" is now next to "Buy milk" but belongs to         │
│  "Walk dog". State did not move with the item.              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Key Choice Decision Tree

┌─────────────────────────────────────────────────────────────┐
│  CHOOSING A KEY                                             │
│                                                             │
│  Does the item have a unique ID?                            │
│    │                                                        │
│    ├── YES ──▶ Use the ID                                   │
│    │                                                        │
│    └── NO                                                   │
│          │                                                  │
│          ▼                                                  │
│  Is there a combination of fields that is unique?           │
│    │                                                        │
│    ├── YES ──▶ Use a composite key                          │
│    │                                                        │
│    └── NO                                                   │
│          │                                                  │
│          ▼                                                  │
│  Is the content unique and immutable?                       │
│    │                                                        │
│    ├── YES ──▶ Use the content as key                       │
│    │                                                        │
│    └── NO                                                   │
│          │                                                  │
│          ▼                                                  │
│  Is the list static (never reorders)?                       │
│    │                                                        │
│    ├── YES ──▶ Index is acceptable                          │
│    │                                                        │
│    └── NO ──▶ Generate a stable ID in the data              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

State Preservation with Keys

┌─────────────────────────────────────────────────────────────┐
│  WITH STABLE KEYS                                           │
│                                                             │
│  Reorder: [A, B, C] → [C, A, B]                             │
│                                                             │
│  ┌─────────┐         ┌─────────┐                            │
│  │ A state │ ──────▶ │ A state │  State moves with A        │
│  │ B state │ ──────▶ │ B state │  State moves with B        │
│  │ C state │ ──────▶ │ C state │  State moves with C        │
│  └─────────┘         └─────────┘                            │
│                                                             │
│  Each item keeps its state. Inputs show correct values.     │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  WITH INDEX KEYS                                            │
│                                                             │
│  Reorder: [A, B, C] → [C, A, B]                             │
│                                                             │
│  ┌─────────┐         ┌─────────┐                            │
│  │ A state │ ──────▶ │ A state │  Position 0 keeps state    │
│  │ B state │ ──────▶ │ B state │  Position 1 keeps state    │
│  │ C state │ ──────▶ │ C state │  Position 2 keeps state    │
│  └─────────┘         └─────────┘                            │
│                                                             │
│  State stays with positions. Inputs mismatch labels.        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
PurposeMatch elements between renders
ScopeSiblings only
TypeString or number
Passed to componentNo
Required for listsYes
Ideal keyDatabase ID
Composite keyCombine fields with separator
Index keyOnly for static lists
Random keyNever
Duplicate keysUnpredictable behavior
Stable key benefitState follows item, efficient DOM moves

Key takeaways:

  • Keys are identity hints for React’s reconciliation algorithm. They tell React which elements in a list are the same between renders, enabling it to preserve DOM nodes and component state for items that have not changed.
  • Keys are not passed to components. React extracts the key prop before rendering. A component cannot read props.key. Pass the ID as a separate prop if the component needs it.
  • Keys must be unique among siblings. Two elements in the same list cannot share a key. Duplicate keys produce undefined behavior, including lost or shared state.
  • Stable keys preserve state. When a list reorders, elements with stable keys move with their items, and state stays attached to the right data. Index keys keep state in position, causing inputs and other stateful elements to show incorrect values.
  • The index key trap is the most common key bug. Using the array index as a key works for static lists but breaks as soon as the list reorders, filters, or changes length. Use a stable ID whenever one exists.
  • Composite keys work when no single field is unique. Combine fields with a separator that cannot appear in any field. Avoid separators that could cause collisions.
  • Random keys break everything. A key that changes on every render tells React that every element is new, so React destroys and recreates the entire list. This loses state and hurts performance.
  • Index keys are acceptable only for genuinely static lists. A list that never reorders, never filters, and never changes length can use the index safely. Even then, a stable key is usually available and preferable.

Remember: The key prop is not boilerplate. It is the mechanism that lets React match elements between renders, and its stability determines whether component state follows the data or stays behind in the wrong position. Choose a key that identifies the item, not its position. Use a database ID when one exists, a composite key when it does not, and the index only for lists that never change. Test with reordering to catch bugs that only appear when the list order shifts. The extra care is small; the bugs it prevents are subtle and expensive.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!