React Unique Key Prop Warning: Fix It Right
React's unique key warning means list items lack stable identity.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓React lists and map rendering basics
- ✓Component state and props comfort
- ✓A React dev environment with console
- 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.
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.
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.
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.
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.
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.
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.
Index Keys Shipped 214 Orders to Wrong Addresses
- 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.
${orderId}-${lineId}.| File | Command / Code | Purpose |
|---|---|---|
| identity-match.js | function byPosition(prev, next) { | Reconciliation Identity |
| index-scramble.js | const rows = [{ id: 'a', text: 'typed-a' }, { id: 'b', text: 'typed-b' }]; | Index-as-Key |
| stable-ids.js | function makeItem(name, id) { | Stable Ids |
| fragment-keys.js | const groups = [ | Keys on Fragments, Nested Lists, and Components |
| dedupe-keys.js | function ensureUnique(items, keyFn) { | When the Warning Lies |
Key takeaways
Common mistakes to avoid
5 patternsUsing key={index} because the list is static today
Generating random keys inside render
Reading key as a prop inside the child
Keying inner cells but not fragment groups
Trusting API ids to be unique
Interview Questions on This Topic
What are React keys for?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's React.js. Mark it forged?
5 min read · try the examples if you haven't