Fix: React Warning — Each Child in a List Should Have a Unique key Prop
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 updatesOr 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:
- Missing key warning — React explicitly tells you it can’t safely reconcile the list without keys.
- 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 intactFix 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 itemWhen 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 addedReact DevTools — Profiler shows key-related rerenders:
- Open React DevTools → Profiler
- Record a list interaction (add, delete, reorder)
- Look for unexpected “unmount + mount” pairs — these indicate key instability
- 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React useTransition Not Working — UI Still Freezes, isPending Never True, or Transition Not Deferred
How to fix React useTransition and startTransition issues — what counts as a transition, Suspense integration, concurrent rendering requirements, and common mistakes that prevent transitions from deferring.
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.