Fix: React Warning: Each child in a list should have a unique "key" prop
Part of: React & Frontend Errors
Quick Answer
How to fix the React unique key prop warning caused by missing keys in lists, duplicate keys, index keys, nested maps, and dynamic list rendering issues.
The Console Warning React Will Not Let Go
Personally, I think the key prop warning is React’s most under-respected console message. Developers often add key={index} to silence it, then spend a future afternoon debugging stale input values they cannot explain. The fix is editorial: use a STABLE unique ID from your data, never the array index for anything that reorders. I learned to take this warning seriously after one too many of those debugging sessions. You render a list in React and see in the browser console:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `UserList`.Or:
Warning: Each child in a list should have a unique "key" prop.
See https://reactjs.org/link/warning-keys for more information.React needs a unique key prop on every element rendered inside an array (usually from .map()). Without keys, React cannot efficiently track which items changed, were added, or were removed.
Quick Reference Before You Dive In
If you arrived here from Google with a fresh key warning, the five facts that resolve roughly 90 percent of cases:
- Use a STABLE unique ID from your data, NOT the array index. Index keys silently corrupt component state when items reorder. The React keys documentation and the
keyprop reference are the canonical sources. - The
keygoes on the OUTERMOST element returned by.map(). Putting it on an inner element does not satisfy React. Math.random()andDate.now()are NEVER valid keys. They produce a new key every render, destroying and recreating every element on each render.- Fragments need keys via
<React.Fragment key={...}>(not<>...</>). The shorthand cannot accept a key. - Keys only need to be unique among SIBLINGS. Different categories can have items with the same key; only conflicts within the same
.map()matter.
The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.
How React Uses Keys for Reconciliation
When React re-renders a list, it needs to match old elements to new elements. Without keys, React compares elements by position; if you insert an item at the beginning, React thinks every item changed because they all shifted down. This causes unnecessary re-renders and can break component state.
Keys give React a stable identity for each list item. When an item moves, React matches it by key and efficiently updates only what changed.
This warning appears when:
- No
keyprop is provided on list elements. - Duplicate keys exist: two elements have the same key.
- Keys are not stable: they change between renders (e.g., using
Math.random()).
When to Use Which Fix
The next nine sections cover the fixes in detail. The table below maps your situation to the recommended fix.
| Your situation | Recommended fix | Why |
|---|---|---|
| Data has unique IDs | Fix 1: key={item.id} | Standard pattern |
| Tempted to use array index | Fix 2: use a stable ID instead | Index corrupts state on reorder |
| Key on inner element | Fix 3: move key to outer element of .map() | React expects it on the returned element |
| Two items share same key | Fix 4: use a truly unique field, or combine | React refuses duplicates |
Nested .map() calls | Fix 5: keys at each level | Siblings only |
| Conditional rendering changes key | Fix 6: keep key stable across conditions | Changing key unmounts component |
| Fetching from an API | Fix 7: use the returned id field | Database IDs are stable |
| Want to force a remount | Fix 8: change the key intentionally | Resets internal state |
| Cannot tell which list is the problem | Fix 9: React DevTools Profiler | Watch for unmount + mount pairs |
If multiple rows apply, pick the topmost one.
Fix 1: Add a Unique Key from Your Data
The best key is a unique identifier from your data: an ID, slug, or other stable unique field.
Broken:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li>{user.name}</li> // Missing key!
))}
</ul>
);
}Fixed:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}The key must be:
- Unique among siblings (not globally unique, just within the list)
- Stable: the same item always gets the same key
- A string or number: not an object
A specific trap I have walked into: assuming key is a regular prop accessible inside the child component. React CONSUMES the key internally and does NOT pass it down. If your component needs the ID, pass it as a separate prop: <UserCard key={user.id} userId={user.id} />. The duplication looks redundant but is correct.
Fix 2: Avoid Using Array Index as Key
Using the array index as a key is a common but often problematic workaround:
{users.map((user, index) => (
<li key={index}>{user.name}</li>
))}This silences the warning but causes bugs when:
- Items are reordered (drag-and-drop, sorting)
- Items are inserted or removed from the middle
- Items have internal state (input values, expanded/collapsed state)
When index keys are OK:
- The list is static and never reorders
- Items have no internal state
- Items are never inserted or removed from the middle
When index keys cause bugs:
// Each item has an input with internal state
{items.map((item, index) => (
<div key={index}>
<input defaultValue={item.name} />
<button onClick={() => removeItem(index)}>Delete</button>
</div>
))}If you delete item 1, React reassigns keys: what was key=2 becomes key=1. React reuses the DOM element for key=1, including its input state. The input now shows the wrong value.
Fix: Use a stable unique identifier. If your data has no IDs, generate them when the data is created:
const [items, setItems] = useState(
initialItems.map(item => ({ ...item, id: crypto.randomUUID() }))
);For client-only ephemeral items (form rows, draft entries) you can also use React 18’s useId:
import { useId } from 'react';
function NewRow({ onAdd }) {
const id = useId(); // stable across renders, unique per component instance
return <input onChange={e => onAdd({ id, value: e.target.value })} />;
}The key point is stability: the key for a given logical item must not change between renders.
Fix 3: Fix Keys on Component Lists
The key goes on the outermost element returned by .map(), not on an inner element:
Broken: key on inner element:
{users.map(user => (
<div>
<span key={user.id}>{user.name}</span> // Wrong: key is on inner element
</div>
))}Fixed: key on the outer element:
{users.map(user => (
<div key={user.id}>
<span>{user.name}</span>
</div>
))}With fragments:
{users.map(user => (
<React.Fragment key={user.id}>
<dt>{user.name}</dt>
<dd>{user.email}</dd>
</React.Fragment>
))}Note: The shorthand fragment syntax <>...</> does not accept a key prop. You must use <React.Fragment key={...}> when you need a key on a fragment.
Fix 4: Fix Duplicate Keys
Two items with the same key confuse React. It logs a separate warning:
Warning: Encountered two children with the same key, `user-1`.Common cause: non-unique field used as key:
// If multiple users share the same name, keys are duplicated
{users.map(user => (
<li key={user.name}>{user.name}</li>
))}Fixed: use a truly unique field:
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}If no unique field exists, combine fields:
{users.map((user, index) => (
<li key={`${user.name}-${user.email}-${index}`}>{user.name}</li>
))}Fix 5: Fix Keys in Nested Maps
When you have nested .map() calls, each level needs its own keys:
{categories.map(category => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}Keys only need to be unique among siblings — items in different categories can have the same key without issues.
Fix 6: Fix Keys with Conditional Rendering
When conditionally rendering list items, make sure keys are consistent:
Broken: key changes based on condition:
{items.map((item, index) => (
<div key={item.isSpecial ? `special-${index}` : index}>
{item.name}
</div>
))}If isSpecial changes, the key changes, and React unmounts and remounts the element; losing all state.
Fixed: use a stable key regardless of state:
{items.map(item => (
<div key={item.id}>
{item.name}
</div>
))}A pattern I have seen in production code that should never have shipped: key={Math.random()} or key={Date.now()}. Each render produces a new key, React destroys and recreates every element, internal state vanishes, and the list re-mounts every keystroke. I now flag any non-deterministic expression in a key as a code review red.
// NEVER DO THIS:
<li key={Math.random()}>{item.name}</li>Fix 7: Fix Keys When Rendering Arrays from APIs
API responses often have IDs that make perfect keys:
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("/api/posts")
.then(res => res.json())
.then(setPosts);
}, []);
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}If the API does not return IDs, ask the backend team to add them. IDs are almost always available in database-backed APIs.
If the API response is not what you expect, see Fix: Objects are not valid as a React child for rendering issues with API data.
For data fetching issues with useEffect, see Fix: React useEffect infinite loop.
Fix 8: Understand When Keys Reset State
Keys are not just for lists. You can use them to force a component to remount:
// Changes to userId reset the entire Profile component
<Profile key={userId} userId={userId} />When the key changes, React unmounts the old component and mounts a new one. This resets all internal state, including useState, useRef, and uncontrolled form inputs.
This is useful when you want a component to “start fresh” when its data source changes, instead of trying to update in place.
Fix 9: Detect Key Instability with React DevTools
If you suspect keys are unstable but cannot tell which list is at fault, open React DevTools → Profiler:
- Record an interaction (add, delete, reorder a list item).
- Look for unexpected unmount + mount pairs (components disappearing and reappearing in the same frame).
- A stable-key list shows “rendered because parent rendered” instead. A full unmount-mount pair almost always means the key changed for that item.
A specific rule I keep top of mind: appending to the END of an index-keyed list is harmless because the existing indices do not change. Prepending (.unshift()) shifts every index and React rebuilds the entire list. If your list is append-only and items have no state, index keys are fine; if either condition fails, use a stable ID.
Stranger Causes I Have Tracked Down
If the warning persists after adding keys:
Check that the key is on the right element. React expects the key on the element returned by .map(), not on a nested child.
Check for arrays of arrays. If you have [[item1, item2], [item3, item4]], each inner array’s items need keys too.
Check for string children with .split(). Splitting a string into an array and rendering it triggers the key warning:
// Triggers warning:
{"Hello World".split(" ").map(word => <span>{word} </span>)}
// Fixed:
{"Hello World".split(" ").map((word, i) => <span key={i}>{word} </span>)}Index keys are fine here because the split result is always in the same order.
Check for React.Children.toArray(). If you dynamically construct children, wrap them:
const children = React.Children.toArray(dynamicChildren);This automatically assigns keys to children that do not have them.
What Other Tutorials Get Wrong About React Keys
Most React tutorials list the same fixes but frame them in ways that produce subtle bugs.
They recommend key={index} as the standard pattern. Index keys silently break component state when items reorder or get prepended. Articles that show index keys without flagging the reorder problem train readers on a pattern that produces invisible bugs.
They miss that key is consumed by React. The key prop is NOT passed down to the component. Articles that show <UserCard key={user.id} /> and then read props.key inside the child produce undefined. Pass the ID as a separate prop.
They omit the fragment shorthand caveat. <>...</> does NOT accept a key. Use <React.Fragment key={...}> when you need a fragment with a key. Articles that show shorthand fragments in .map() examples produce silent warnings.
They miss the intentional remount pattern. Changing key deliberately resets a component (<Profile key={userId} />). This is a feature, not a bug. Articles that present key changes as always bad miss this legitimate use case.
They miss useId for client-only ephemeral lists. Form rows added in-session, drafts, etc. need stable IDs but have none from the server. useId is the modern answer; articles that recommend Math.random() for these cases train an antipattern.
They miss React.Children.toArray. It auto-assigns keys to dynamically-constructed children. Articles that focus only on .map() miss this less-common but useful API.
Frequently Asked Questions
Why is using the array index as a key bad?
When the list reorders (sort, drag, prepend, insert, delete from middle), the index for a given item changes. React reuses the DOM element for index 0 even when the underlying data is different, leading to stale input values, wrong checked states, and other state-related bugs. Index keys are only safe when the list never reorders AND items have no internal state.
Can I use a UUID generated inline as a key?
Only if you generate it ONCE per item and store it on the item. key={crypto.randomUUID()} generated inline in .map() produces a NEW UUID every render, which is the same as Math.random() and just as broken. Generate the UUID when the item is created and persist it.
Does the key need to be globally unique?
No, only unique among SIBLINGS within the same .map(). Items in different lists or different parents can have the same key. The uniqueness scope is the parent’s children, not the whole app.
Why doesn’t React just use the position automatically?
Because position is unreliable. When items insert or reorder, React cannot tell whether item at position 2 is “the same item that moved” or “a brand-new item that happens to be at position 2.” The key is your declaration of identity; without it, React falls back to position and produces the bugs you see.
Can the key be a string or only a number?
Either works. React accepts any primitive value (string, number, bigint) as a key. Database IDs as strings (UUIDs, ULIDs, slugs) are perfectly valid keys.
Does the warning disappear in production builds?
No. The warning is from React itself and fires in both dev and production. The bugs caused by missing or unstable keys also persist in production; the warning is the only way React tells you about them before they bite.
For performance issues caused by large lists re-rendering, see Fix: React too many re-renders. For hook-related issues in list rendering, see Fix: React hooks called conditionally.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
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: Docusaurus Not Working — Build Failing, Sidebar Not Showing, or Plugin Errors
How to fix Docusaurus issues — docs and blog configuration, sidebar generation, custom theme components, plugin setup, MDX compatibility, search integration, and deployment.
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.