Skip to content

Fix: React Warning — Each Child in a List Should Have a Unique key Prop

FixDevs ·

Quick Answer

How to fix React's missing key prop warning — why keys matter for reconciliation, choosing stable keys, avoiding index as key pitfalls, keys in fragments, and performance impact.

The Problem

React logs a warning in the console:

Warning: Each child in a list should have a unique "key" prop.
Check the render method of `UserList`.

Or keys are added but the warning persists:

{items.map((item, index) => (
  <Item key={index} item={item} />  // No warning — but bugs appear when reordering
))}

Or list items rerender unexpectedly after sorting or filtering:

// Items rerender with incorrect state after filtering
// A form input loses its value when the list is sorted
// Animations trigger incorrectly on list updates

Or a <Fragment> inside a list needs a key:

{items.map(item => (
  <>                         {/* Warning: missing key on Fragment */}
    <dt>{item.label}</dt>
    <dd>{item.value}</dd>
  </>
))}

Why This Happens

React uses keys to track which list items have changed, been added, or been removed between renders. During reconciliation, React compares the current virtual DOM against the previous one. Without keys, React can only compare items by position — it assumes the first item is always the same “first item,” the second is always the same “second item,” and so on.

This causes two categories of problems:

  1. Missing key warning — React explicitly tells you it can’t safely reconcile the list without keys.
  2. Stale state bugs — when a keyed list item moves (sort, filter, reorder), React uses the item’s identity to preserve its state. Without stable keys (or with index-as-key), React matches items by position and may apply the wrong state to the wrong component.

Example of the stale state bug:

// List of items — each with an input inside
// Items: [A, B, C]
// User types "hello" in A's input
// User sorts — now: [C, A, B]
// With index keys: React sees position 0 still exists — keeps "hello" in C's input (wrong!)
// With stable ID keys: React tracks A by ID — moves A with its "hello" text intact

Fix 1: Add Keys from Stable Data IDs

The best key is a stable, unique identifier from your data:

// CORRECT — use database ID or unique identifier
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>   {/* Stable unique ID */}
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// CORRECT — string or number both work as keys
function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <ProductCard
          key={product.sku}     {/* SKU is a stable unique string */}
          product={product}
        />
      ))}
    </div>
  );
}

// CORRECT — composite key when no single field is unique
function OrderItemList({ orderItems }) {
  return (
    <table>
      <tbody>
        {orderItems.map(item => (
          <tr key={`${item.orderId}-${item.productId}`}>  {/* Composite key */}
            <td>{item.productName}</td>
            <td>{item.quantity}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Fix 2: Understand When Index Keys Cause Bugs

Using the array index as a key is only safe in specific scenarios:

// UNSAFE — index key with a mutable list
function TodoList({ todos, onDelete }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem
          key={index}     // Index shifts when items deleted — wrong item gets state
          todo={todo}
          onDelete={() => onDelete(index)}
        />
      ))}
    </ul>
  );
}

// SAFE — index key with a static, never-reordered list
function StaticList({ items }) {
  return (
    <ol>
      {items.map((item, index) => (
        // Safe because:
        // 1. List is never reordered or filtered
        // 2. Items have no local state
        // 3. Items are purely presentational
        <li key={index}>{item.label}</li>
      ))}
    </ol>
  );
}

The index-as-key stale state problem visualized:

// Imagine 3 inputs with index keys: 0="A", 1="B", 2="C"
// User types "hello" in the first input (index 0, item A)
// Item A is removed from the list
// New list: 0="B", 1="C"
// React sees key=0 still exists — reuses the DOM node from old key=0 (item A)
// "hello" is now in item B's input — wrong state, wrong item

When index as key is acceptable:

  • The list is static and never changes order
  • Items have no internal state (pure display)
  • The list is never filtered or sorted

Fix 3: Fix Keys on Fragments

<>...</> shorthand syntax doesn’t support the key prop. Use <Fragment> explicitly:

import { Fragment } from 'react';

// WRONG — shorthand fragment doesn't accept key
{items.map(item => (
  <>
    <dt key={item.id}>{item.label}</dt>   {/* Key on child, not fragment */}
    <dd>{item.value}</dd>
  </>
))}
// Warning: Each child in a list should have a unique key prop

// CORRECT — explicit Fragment with key
{items.map(item => (
  <Fragment key={item.id}>              {/* Key on the Fragment */}
    <dt>{item.label}</dt>
    <dd>{item.value}</dd>
  </Fragment>
))}

// Real-world example — definition list
function GlossaryList({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        <Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Fix 4: Generate Stable Keys for Data Without IDs

When data doesn’t have a natural ID, generate stable keys:

// Option 1 — use a unique property combination
function TagList({ tags }) {
  return (
    <div>
      {tags.map(tag => (
        // Tags are unique by name — name is stable
        <span key={tag.name} className="tag">{tag.name}</span>
      ))}
    </div>
  );
}

// Option 2 — assign IDs when fetching/creating data (preferred)
async function fetchItems() {
  const items = await api.getItems();
  // Items from API have IDs — use them directly
  return items;
}

// Option 3 — generate IDs on the client for new items before they're saved
import { useId } from 'react';  // React 18+

function NewItemForm({ onAdd }) {
  // useId generates a stable, unique ID per component instance
  const id = useId();

  function handleAdd(value) {
    onAdd({ id, value });   // Use this ID as the key
  }
}

// Option 4 — for ephemeral lists (search results, etc.)
// Generate a stable key from the content
function SearchResults({ results }) {
  return (
    <ul>
      {results.map(result => (
        <li key={`${result.type}-${result.slug}`}>  {/* Stable composite */}
          {result.title}
        </li>
      ))}
    </ul>
  );
}

Avoid generating keys with Math.random() or Date.now():

// WRONG — new key every render causes remount on every render
{items.map(item => (
  <Item key={Math.random()} item={item} />   // Breaks memoization and state
))}

// WRONG — crypto.randomUUID() also generates new keys each render
{items.map(item => (
  <Item key={crypto.randomUUID()} item={item} />
))}

Fix 5: Using Keys to Force Component Remounts

A non-obvious but useful pattern — change a key deliberately to reset a component’s state:

// Form reset — changing key destroys and recreates the component
function UserEditor({ userId }) {
  return (
    // When userId changes, a completely fresh form mounts with clean state
    <UserForm key={userId} userId={userId} />
  );
}

// Without key trick — stale form state persists when switching users
function UserEditorBuggy({ userId }) {
  return <UserForm userId={userId} />;  // Old form state lingers when userId changes
}

Resetting an animation or component on data change:

function AnimatedScore({ score }) {
  return (
    // New key triggers remount — animation runs fresh each time score changes
    <ScoreCounter key={score} initialValue={score} />
  );
}

Pro Tip: The key-change-to-reset pattern is an intentional use of React’s reconciliation. It’s far simpler than manually resetting state with useEffect.

Fix 6: Keys in Nested Lists

Nested lists need keys at every level:

function CategoryList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>                {/* Key on outer item */}
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>   {/* Key on inner item */}
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

Keys must be unique among siblings — not globally:

// CORRECT — same key value in different lists is fine
// Category A items: key=1, key=2, key=3
// Category B items: key=1, key=2, key=3
// These are separate lists — no conflict

function SafeNestedList({ categories }) {
  return (
    <>
      {categories.map(category => (
        <section key={category.id}>
          <h2>{category.name}</h2>
          {category.items.map(item => (
            // item.id=1 in category A and item.id=1 in category B: fine
            // They're siblings within their own parent, not the same parent
            <div key={item.id}>{item.name}</div>
          ))}
        </section>
      ))}
    </>
  );
}

Fix 7: Keys and React Performance

Keys affect reconciliation performance. Well-chosen keys minimize unnecessary DOM operations:

// Adding an item to the END — index keys work fine here
// React sees new item at new index — adds it, doesn't touch others
const items = ['A', 'B', 'C'];
// After push('D'): ['A', 'B', 'C', 'D']
// With index keys: A=0 (same), B=1 (same), C=2 (same), D=3 (new) — efficient

// Adding an item to the START — index keys are catastrophic
// After unshift('Z'): ['Z', 'A', 'B', 'C']
// With index keys: Z=0 (was A), A=1 (was B), B=2 (was C), C=3 (new) — ALL remount

// With stable ID keys: Z=newId (new), A=idA (same), B=idB (same), C=idC (same) — only Z added

React DevTools — Profiler shows key-related rerenders:

  1. Open React DevTools → Profiler
  2. Record a list interaction (add, delete, reorder)
  3. Look for unexpected “unmount + mount” pairs — these indicate key instability
  4. Components that should update will show “rendered because parent rendered” — not full remounts

Still Not Working?

Key on component vs DOM element — the key prop must be on the outermost element returned from the map callback. If your component wraps itself in another element, the key on the inner element doesn’t help:

// WRONG — key on inner div, not on the mapped root
{items.map(item => (
  <div>                        {/* No key — React sees this */}
    <Item key={item.id} />    {/* Key here is ignored for list tracking */}
  </div>
))}

// CORRECT
{items.map(item => (
  <div key={item.id}>         {/* Key on the outermost element */}
    <Item />
  </div>
))}

Keys in conditional rendering — if an item switches between two different component types, React unmounts the old and mounts the new regardless of key. Use a consistent component type, or wrap in a div with the key.

Duplicate keys — React warns about duplicate keys in the same list. If two items have the same ID (data integrity issue), you’ll see the warning even with ID-based keys. Fix the data, or use a composite key.

For related React issues, see Fix: React Memo Not Working and Fix: React useEffect Infinite Loop.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles