Home JavaScript React Unique Key Prop Warning: Fix It Right
Beginner 5 min · September 23, 2026

React Unique Key Prop Warning: Fix It Right

React's unique key warning means list items lack stable identity.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 10 min
  • React lists and map rendering basics
  • Component state and props comfort
  • A React dev environment with console
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • React uses keys to match list items across renders, so missing or unstable keys make it reuse the wrong component state.
  • Index-as-key works only for static lists. Reorders, inserts, and filters shuffle state into the wrong rows.
  • Use stable ids from your data like item.id, and generate ids once at creation for items that lack them.
  • Put keys on the outermost element returned from map, including fragments, and keep them unique among siblings.
✦ Definition~90s read
What is React Unique Key Prop Warning Fix?

Reconciliation is React's process of matching the previous element tree against the next one to decide minimal DOM updates, and keys are the identity hints that make matching correct. When you map an array to elements, React pairs old and new children to preserve component state, focus, and DOM nodes.

Think of a teacher tracking students by desk row instead of by name.

Without keys it pairs by position: first child to first child. With keys it pairs by key, so a moved item keeps its state wherever it lands.

Index-as-key is position pairing with extra steps. It works while the list is append-only and never reorders, because position and identity coincide. The moment an item inserts at the top, filters out the middle, or sorts differently, every index after the change points at a different item.

React then reuses stateful DOM and component instances for the wrong data: typed text stays in the row while the item leaves, focus jumps, and controlled inputs display stale values.

Stable ids solve this by naming identity independently of position. A database id, slug, or UUID assigned once at creation follows the item through every reorder and filter. React matches by that name, moves the existing DOM node, and preserves its state. Keys need uniqueness only among siblings, not globally, and they never reach the component as props: they are consumed by the reconciler itself.

Fragments and nesting follow the same rule at each level. The key belongs on the outermost element the map returns, which for grouped rows means the fragment itself via keyed Fragment syntax. Nested lists need keys at both levels. When the warning persists despite keys everywhere, duplicate ids from the API are the usual cause, and deduplication or composite keys close the gap.

Plain-English First

Think of a teacher tracking students by desk row instead of by name. When two students swap seats, the row-based roster credits the wrong child for every answer. Index keys are desk rows: they describe position, not identity. Stable id keys are names: they follow the student wherever they sit. React reconciles by the roster you give it, so hand it names and reorders stop scrambling state.

The console warns each child in a list should have a unique key prop, your list renders anyway, and you move on. Weeks later, inputs lose focus on reorder, checkboxes tick the wrong rows, and animations jump to strangers. The warning was the smoke. The scrambled state is the fire, and both come from identity React was never given.

Keys look like boilerplate because lists work without them on static data. React falls back to position, and positions match identity exactly until the first reorder, insert, or filter. That delay between cause and symptom is why teams dismiss the warning: the code that breaks the list ships months before the feature that reorders it.

This guide gives keys the ten minutes they deserve. You will learn how reconciliation uses identity to preserve state, why index-as-key fails precisely when lists get interesting, what makes an id stable enough to trust, where keys belong on fragments and nested lists, and what to do when the API sends duplicates. The warning disappears as a side effect of getting identity right.

Reconciliation Identity: Why React Needs Keys

React never rebuilds the DOM from scratch on update. It diffs the new element tree against the old one, reuses matching DOM nodes, and patches only what changed. For single elements the match is structural. For lists it is ambiguous: when five children become six, which old child corresponds to which new one. Keys answer that question explicitly, and the quality of the answer decides whether state survives the update intact.

Without keys, React pairs by position. Insert at the top and every old child shifts one slot: React updates each node's content in place rather than moving nodes, and component state stays glued to positions. With stable keys, React pairs by identity: the inserted item mounts fresh while existing nodes move with their state untouched. Same data change, opposite DOM surgery, wildly different user experience.

State preservation is the concrete payoff. Uncontrolled inputs, focus, scroll position, and animation state all live in DOM nodes or component instances that React either reuses or discards based on matching. Correct keys keep them attached to their item through reorders. Missing keys leave them attached to positions, which is how typed text ends up in the wrong row after a sort.

The snippet models pairing by position versus by id on plain data. Position pairing rewrites every slot after an insert while id pairing moves one entry. React performs the same choice internally on every list render, with the same consequences for state.

identity-match.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
function byPosition(prev, next) {
  return next.map((item, i) => ({ slot: i, reused: prev[i], item }));
}
function byId(prev, next) {
  const map = new Map(prev.map((p) => [p.id, p]));
  return next.map((item) => ({ key: item.id, reused: map.get(item.id), item }));
}

const prev = [{ id: 'a' }, { id: 'b' }];
const next = [{ id: 'x' }, { id: 'a' }, { id: 'b' }];
console.log('position slots reused:', byPosition(prev, next).length);
console.log('id keys preserved:', byId(prev, next).filter((r) => r.reused).length);
Try it live
📊 Production Insight
Lists that are static today gain sorting tomorrow in nearly every product. Choosing stable ids at list creation costs nothing and preempts the entire scramble class, while index keys charge the fix with interest later.
🎯 Key Takeaway
Keys tell React which old child matches which new one. Position pairing breaks on reorder while id pairing preserves state through moves.

Index-as-Key: Fine Until You Reorder

Index keys are correct in exactly one situation: the list never reorders, never filters, never inserts anywhere but the end, and items carry no state. Static navigation menus and fixed option lists qualify. Everything else eventually violates one condition, and the violation scrambles silently because React warns about missing keys but never about unstable ones. The dangerous key is the present-but-wrong one.

The failure mechanics are worth internalizing. With key={index}, inserting at position 0 shifts every key-to-item assignment: the component instance formerly known as 0 keeps its state but receives item 1's props. Controlled inputs show this as values jumping rows. Uncontrolled inputs show it as text staying while labels change. Focus and selection follow the position, not the data, which users experience as the UI gaslighting them.

Performance suffers alongside correctness. Positional pairing rewrites every row's DOM content after an insert instead of moving one node, turning an O(1) move into O(n) updates. Large lists feel the difference as visible jank on precisely the interactions, sorting and filtering, that users repeat most.

The snippet demonstrates the scramble: position-kept state pairs with the wrong item after a reorder while id-kept state follows correctly. Run it, then grep your codebase for key={index} and treat every hit outside provably static lists as a bug report.

index-scramble.jsJAVASCRIPT
1
2
3
4
5
6
7
8
const rows = [{ id: 'a', text: 'typed-a' }, { id: 'b', text: 'typed-b' }];
const sorted = [...rows].reverse();

const byIndex = sorted.map((item, i) => ({ key: i, shown: rows[i].text }));
console.log('index keys show:', JSON.stringify(byIndex));

const byId = sorted.map((item) => ({ key: item.id, shown: item.text }));
console.log('id keys show:   ', JSON.stringify(byId));
Try it live
⚠ Present but Wrong Is Silent
React warns for missing keys and stays quiet for index keys, so the absence of warnings proves nothing. Audit key={index} by hand because no console message will do it for you.
📊 Production Insight
The incident's 214 misdirected orders came from a list that was static for months before sorting shipped. Any roadmap containing sort, filter, or drag-and-drop invalidates index keys retroactively, so choose ids before the feature exists.
🎯 Key Takeaway
Index keys describe position, not identity. Inserts, filters, and sorts reassign every key, scrambling state and slowing renders.

Stable Ids: What Makes a Good Key

A good key is stable, unique among siblings, and tied to the item rather than its position. Database ids are ideal: assigned once, never reused, and already present in the data. Slugs and external codes work equally well when uniqueness holds. The key test is survival: the same item must produce the same key across renders, refetches, and reorders, or identity breaks.

Generated ids fill the gap for client-created items. Assign a UUID once when the item is created, at the reducer or submit handler, and store it on the object from birth. Generating inside render is the classic mistake: a fresh random key per render forces React to unmount and remount every row each pass, destroying state and tanking performance worse than any index bug.

Composite keys rescue items without single unique fields. Order lines keyed by order-plus-line, calendar cells by date-plus-slot, and form rows by section-plus-field each gain uniqueness from the combination. Build the composite once where the data is shaped, not inline in JSX, so the formula stays consistent and testable.

Never use array length or timestamps from render for keys. Both change across renders and collide under speed: two items created in the same millisecond share a timestamp key. The snippet shows once-at-creation UUIDs staying stable while per-render randoms churn, which is the whole discipline in miniature.

stable-ids.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
function makeItem(name, id) {
  return { id: id || ('item-' + Math.random().toString(36).slice(2)) , name };
}

const created = [makeItem('apples'), makeItem('pears')];
console.log('created once:', created.map((c) => c.id).join(', '));
console.log('same ids next render:', created.map((c) => c.id).join(', '));

const ids = new Set(created.map((c) => c.id));
console.log('unique among siblings:', ids.size === created.length);
Try it live
📊 Production Insight
Backend pagination that reuses row numbers per page produces duplicate keys when pages concatenate in infinite scroll. Namespace page-local ids with the page or cursor at shaping time, or appended pages will reconcile against each other.
🎯 Key Takeaway
Use database ids where present and assign UUIDs once at creation otherwise. Keys must survive renders, refetches, and reorders unchanged.

Keys on Fragments, Nested Lists, and Components

The key belongs on the outermost element the map callback returns, because that is the unit React matches. When rows group several cells without a wrapper div, the fragment is the outermost element, and shorthand <> fragments cannot carry keys at all. Switch to the explicit Fragment form with the key attached, and the group reconciles as one identity instead of warning and scattering.

Custom components follow the same placement with one subtlety: the key sits on the component tag in the parent's map, and React consumes it before props reach the child. A component cannot read its own key via props, which surprises developers trying to use it as an id. Pass a separate id prop for data needs and let the key serve reconciliation alone.

Nested lists need keys at every level. The outer map keys the groups and each inner map keys its items, with uniqueness required only among siblings at the same level. A table of orders keyed by order id holds line rows keyed by line id, and neither level's keys interfere with the other. Missing the inner keys warns per group and scrambles lines independently.

Conditional wrappers complicate placement: early returns and ternaries that swap element types must still expose the key on whichever outermost element renders. Keep the key on the branch roots rather than burying it inside, or toggling the condition remounts state you meant to preserve.

fragment-keys.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
const groups = [
  { id: 'g1', cells: ['a', 'b'] },
  { id: 'g2', cells: ['c'] }
];

const keyed = groups.map((g) => ({
  fragmentKey: g.id,
  children: g.cells.map((c) => ({ key: g.id + ':' + c, cell: c }))
}));
console.log(JSON.stringify(keyed, null, 1).slice(0, 220) + '...');
console.log('outer keys unique:', new Set(keyed.map((k) => k.fragmentKey)).size === keyed.length);
Try it live
📊 Production Insight
Grouped table rows are the most common fragment-key miss, because developers key the inner cells and leave the group anonymous. The group then remounts per sort while cells keep keys, producing half-preserved state that confuses debugging.
🎯 Key Takeaway
Key the outermost returned element at every map level. Use explicit Fragment syntax for keyed groups and pass separate id props for data.

When the Warning Lies: Duplicate Keys From the API

Sometimes keys exist everywhere and rows still mismatch. Duplicated ids from the API are the usual cause: paginated endpoints repeating row numbers, merged feeds sharing identifier spaces, or placeholder ids like 0 and -1 stamped on every draft. React warns about duplicates in development, but the warning drowns in noisy consoles and the production build stays silent while mismatching.

Detect duplicates mechanically at the shaping layer. Compare the id set size against the array length right where API data enters the store, and log the offending values in development. The check costs one line and names the liar instantly instead of letting reconciliation symptoms masquerade as component bugs.

Fix upstream when possible: ask the API for truly unique ids or a cursor-scoped namespace. When the backend cannot change, composite at the boundary by combining source and id, page and row, or type and slug. The composite must be built once at ingestion so every consumer shares the same identity, never recomputed per render where formulas drift.

The snippet implements the boundary check plus composite repair. Run your suspect payload through it and duplicates confess immediately. Promote the check into a dev-only assertion so regressions announce themselves at the data layer instead of the DOM.

dedupe-keys.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ensureUnique(items, keyFn) {
  const seen = new Map();
  for (const item of items) {
    const k = keyFn(item);
    if (seen.has(k)) console.log('duplicate key:', k);
    seen.set(k, item);
  }
  return seen.size === items.length;
}

const feed = [{ id: 7 }, { id: 7 }, { id: 9 }];
console.log('unique?', ensureUnique(feed, (i) => i.id));
const fixed = feed.map((i, n) => ({ ...i, key: 'p1:' + i.id + ':' + n }));
console.log('composite unique?', ensureUnique(fixed, (i) => i.key));
Try it live
💡Assert Uniqueness at Ingestion
Check id uniqueness where API data enters your store, not where lists render. One assertion at the boundary protects every list downstream and names duplicates while the payload is still in hand.
📊 Production Insight
Merged feeds from acquisitions duplicate ids across identifier spaces silently for months. Namespace each source at ingestion from day one of the merge, or reconciliation bugs will be attributed to every team except the data layer.
🎯 Key Takeaway
Duplicate ids reconcile as one identity. Detect at ingestion with a set-size check and repair with composites built once at the boundary.

Key Hygiene Checklist for Every List

Review every map call against five questions before merging. Does each element carry a key on its outermost node. Is the key a stable id rather than an index, random, or timestamp. Are keys unique among siblings, verified rather than assumed. Do fragments use the explicit keyed form. Are composites built once at ingestion for id-less or duplicated data. Five yes answers mean the list survives sorting, filtering, and pagination.

Encode the checklist in tooling so humans cannot skip it. Lint rules flag key={index} and missing keys at build time. A test helper asserts key stability by rendering twice and comparing. Integration tests type into rows, reorder, and assert pairing follows identity. Each control converts one checklist line from discipline into default.

Teach the roster analogy to newcomers because it sticks. Desk rows versus names explains in seconds why positions fail and ids hold. Developers who internalize the analogy choose stable ids reflexively, and reflexive choices need no checklist.

Revisit old lists on a schedule. Code written before sorting existed carries index keys that the roadmap will detonate. A quarterly grep for key={index} plus the reorder-and-type test on flagged lists keeps the backlog honest and the shipments addressed correctly.

📊 Production Insight
Quarterly key audits pair well with roadmap reviews: every planned sort, filter, or drag-and-drop feature names the lists it endangers. Audit those lists before the feature ships, not after customers report scrambled rows.
🎯 Key Takeaway
Key outermost nodes with stable unique ids, verify with lint and reorder tests, and re-audit old lists whenever sorting or filtering ships.
● Production incidentPOST-MORTEMseverity: high

Index Keys Shipped 214 Orders to Wrong Addresses

Symptom
Over 6 days, 214 of 31,000 orders shipped to mismatched addresses while the checkout total stayed correct. Support saw customers insisting they typed the right address, and session replays showed correct typing followed by a sort click that visibly shuffled inputs into wrong rows. The bug survived because totals and payments never erred, so no financial alert fired.
Assumption
The team assumed inputs were bound to address objects because the onChange handlers updated the right ids. Reviewers approved index keys since the list initially rendered in fixed order with no sorting. The sort feature shipped 3 weeks later in a separate PR that nobody connected to the key choice.
Root cause
The address rows used key={index} with uncontrolled inputs holding typed text in DOM state. Clicking sort reordered the data while React reused DOM nodes by position, so row 0's typed street stayed in row 0 while a different address moved there. Checkout submitted the shuffled pairing for 0.7 percent of order volume, concentrated in multi-address business accounts that sort most.
Fix
The list switched to key={address.id} with controlled inputs bound to address objects, so state follows identity through any reorder. The team added a reorder-and-type Cypress test that sorts mid-typing and asserts pairing, plus an ESLint rule flagging key={index} patterns. Affected customers got reshipments and the 214 orders were reconciled over 4 days.
Key lesson
  • Index keys are deferred bugs that detonate when sorting ships. Any list that might ever reorder needs stable ids from day one.
  • Uncontrolled inputs amplify key bugs because state lives in DOM nodes React reuses. Controlled inputs bound to ids keep data and identity together.
  • A sort-mid-typing integration test catches what unit tests cannot. Reorder behavior needs at least one test that types, sorts, then asserts pairing.
Production debug guideFive steps that trace scrambled list state back to the guilty map call.5 entries
Symptom · 01
Console shows each child in a list should have a unique key prop
Fix
Click the warning's component stack to find the exact map call. Check whether the elements carry any key at all: a missing key warns immediately, while a present-but-unstable key warns nothing and scrambles silently. Fix missing keys first since they are loud.
Symptom · 02
Inputs or focus scramble on reorder, insert, or filter
Fix
Search the list for key={index} or key={i}. That pattern is the prime suspect whenever state sticks to positions instead of items. Switch to the item's stable id and retest the exact reorder that scrambled. If no reorder exists yet, add one to prove the fix.
Symptom · 03
Keys exist but rows still mismatch
Fix
Log the key values for duplicates: items with repeated ids reconcile as one identity. Confirm with new Set(items.map(i => i.id)).size !== items.length. Deduplicate at ingestion or build composite keys like ${orderId}-${lineId}.
Symptom · 04
The warning points at a fragment or wrapper
Fix
Move the key to the outermost returned element. Shorthand <> fragments accept no keys, so switch to <Fragment key={id}>. When map returns a custom component, the key sits on that component tag, never inside it as a prop.
Symptom · 05
You need proof the fix preserves identity
Fix
Type distinct text into several rows, reorder the list, and assert each text followed its item. Automate it: a Cypress or Playwright test that types, sorts, and checks pairing. Promote it to CI so the next index-key refactor fails loudly.
React Key Strategies Compared
Root CauseHow to ConfirmFixPrevention
Missing key on list itemsConsole warns with the map's component stackAdd key={item.id} on the outer elementLint rule failing builds on unkeyed maps
Index-as-key on dynamic listsState sticks to positions after reorderSwitch to stable ids from dataForbid key={index} outside static lists
Per-render random keysEvery row remounts and loses state each passGenerate ids once at item creationCreate ids in reducers, never in render
Duplicate ids from the APISet size smaller than array lengthDeduplicate or composite at ingestionUniqueness assertion where data enters
Key buried inside the componentWarning persists despite keys in JSXMove key to the outermost mapped tagReview checklist on fragment and wrapper maps
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
identity-match.jsfunction byPosition(prev, next) {Reconciliation Identity
index-scramble.jsconst rows = [{ id: 'a', text: 'typed-a' }, { id: 'b', text: 'typed-b' }];Index-as-Key
stable-ids.jsfunction makeItem(name, id) {Stable Ids
fragment-keys.jsconst groups = [Keys on Fragments, Nested Lists, and Components
dedupe-keys.jsfunction ensureUnique(items, keyFn) {When the Warning Lies

Key takeaways

1
Keys are identity hints that let reconciliation preserve state through list changes.
2
Index keys describe position and scramble state on any reorder, insert, or filter.
3
Use stable ids, assigning UUIDs once at creation when data lacks them.
4
Key the outermost returned element, including explicit Fragments for groups.
5
Duplicates reconcile as one identity, so assert uniqueness at ingestion.
6
Prove fixes with type-sort-assert tests and lint rules that fail bad keys.

Common mistakes to avoid

5 patterns
×

Using key={index} because the list is static today

Symptom
Sorting ships months later and state scrambles across rows with no new warning.
Fix
Key by stable id from day one. Roadmaps always add reordering eventually.
×

Generating random keys inside render

Symptom
Every row unmounts and remounts each pass, destroying focus and tanking performance.
Fix
Assign the id once at item creation and store it on the object.
×

Reading key as a prop inside the child

Symptom
props.key is undefined and the child cannot use its own identifier.
Fix
Pass a separate id prop for data. React consumes key before props arrive.
×

Keying inner cells but not fragment groups

Symptom
Groups remount on sort while cells keep keys, half-preserving row state.
Fix
Use explicit Fragment with key={group.id} on the outermost returned node.
×

Trusting API ids to be unique

Symptom
Paginated or merged feeds reconcile distinct rows as one identity.
Fix
Assert uniqueness at ingestion and composite with source or page where needed.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What are React keys for?
Q02JUNIOR
Why is index-as-key harmful on reorderable lists?
Q03SENIOR
Why does generating keys with Math.random in render break lists?
Q04SENIOR
Can a child component read its own key prop?
Q05SENIOR
Keys exist but rows still mismatch. How do you investigate?
Q01 of 05JUNIOR

What are React keys for?

ANSWER
Keys give list items stable identity across renders so reconciliation pairs old children with new ones correctly. Correct pairing preserves component state, focus, and DOM nodes through inserts, deletes, and reorders.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Do keys need to be globally unique?
02
Are index keys ever acceptable?
03
Why does the warning persist after I added keys?
04
Do keys affect performance?
05
Should keys come from the database?
06
How do nested lists handle keys?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's React.js. Mark it forged?

5 min read · try the examples if you haven't

Previous
React Maximum Update Depth Fix
50 / 50 · React.js
Next
Node Cannot Find Module Fix