Why Keys in React Are Critical for List Performance and State Preservation
Keys in React provide stable identity to list elements, enabling the reconciler to match Fibers across renders, preserve component state during reordering, and minimize DOM mutations.
React uses the key property to optimize the reconciliation process when rendering dynamic lists. According to the facebook/react source code, each element's key is stored on its corresponding Fiber node (fiber.key) and read during the reconciliation phase to determine which components can be reused, moved, or destroyed. Without stable keys, React falls back to index-based comparison, leading to unnecessary unmounting, state loss, and degraded rendering performance.
How React Uses Keys During Reconciliation
When React renders an array of elements, it constructs an internal Fiber tree representing the UI state. During subsequent renders, the reconciler must diff the new array against the previous one to identify changes.
In packages/react-reconciler/src/ReactFiberWorkLoop.js, the algorithm extracts the key from each element and groups children by that key before performing the diff. This allows React to match a specific element in the new list to the exact Fiber instance that produced it previously, even if the element's position in the array has changed.
Without explicit keys, React resorts to index-based matching, assuming the first element of the new list corresponds to the first element of the old list. This approach fails whenever items are inserted, deleted, or reordered, causing React to treat existing components as new instances.
Consequences of Missing or Unstable Keys
Using array indices or omitting keys entirely creates specific problems in React applications:
- State loss in reordered lists: Stateful components lose their local state (including
useStateanduseRefvalues) when React mistakes them for new instances during reordering operations. - Unnecessary DOM mutations: React may delete and recreate DOM nodes that could have been moved, resulting in slower rendering and loss of DOM state like focus or scroll position.
- Broken animations: In animation or transition scenarios, React cannot distinguish between elements, potentially animating the wrong component or triggering incorrect enter/exit transitions.
- Debugging difficulties: As noted in
packages/react-devtools/OVERVIEW.md, the DevTools relies on keys to accurately display component identity in the tree inspector.
Best Practices for Assigning Keys
The React source code defines the key type as React$Key (string, number, or null) in the internal type definitions. Follow these guidelines to ensure optimal reconciliation:
Stability: Use values that remain constant across renders, such as database IDs or unique identifiers from your data model. Avoid using random values or timestamps generated during render.
Uniqueness among siblings: Keys must be unique within the same array, but they do not need to be globally unique across your application. Two items in the same list must never share the same key.
Primitive types only: Strings and numbers work reliably. Objects are coerced to the string "[object Object]", which destroys uniqueness and causes reconciliation bugs.
Index fallback: Only use array indices as keys when the list is static—meaning items never change order, are never inserted, and are never deleted.
Practical Implementation Examples
Correct Usage with Stable IDs
Always derive keys from stable data properties:
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
// ✅ Unique, stable identifier from data source
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Missing Keys (Anti-pattern)
Omitting the key property forces React to use index matching:
function BadList({ items }) {
return (
<ul>
{items.map(item => (
// ❌ No key – React falls back to index-based matching
<li>{item.label}</li>
))}
</ul>
);
}
Safe Index Usage
Only use indices for truly static lists:
function StaticList({ staticItems }) {
return (
<ul>
{staticItems.map((s, i) => (
// ✅ Safe because the list never changes order or length
<li key={i}>{s}</li>
))}
</ul>
);
}
Preserving State During Reordering
This example demonstrates how stable keys maintain component identity:
function Counter({ id }) {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
#{id}: {count}
</button>
);
}
function ShuffleDemo({ data }) {
const [order, setOrder] = React.useState(data.map(d => d.id));
const shuffle = () =>
setOrder(prev => [...prev].sort(() => Math.random() - 0.5));
return (
<>
<button onClick={shuffle}>Shuffle</button>
<div>
{order.map(id => (
// ✅ Key ties each Counter instance to its logical identity
<Counter key={id} id={id} />
))}
</div>
</>
);
}
Each Counter component retains its count state when shuffled because the stable id key allows React to move the underlying Fiber rather than destroying and recreating it.
Key Files in the React Repository
The significance of keys is reflected throughout the React architecture:
packages/react-reconciler/src/ReactFiberWorkLoop.js: Contains the core reconciliation loop that extractskeyvalues from elements and groups children for efficient diffing.packages/react-reconciler/src/getComponentNameFromFiber.js: Uses thekeyproperty when generating component display names for debugging output.packages/react-devtools/OVERVIEW.md: Documents how DevTools transmits thekeyfield over the bridge protocol for inspection in the component tree.packages/react-devtools/CHANGELOG.md: References specific bug fixes for non-string element keys, demonstrating the critical nature of key handling in production applications.
Summary
- Keys enable Fiber reuse: The reconciler uses
keyvalues inReactFiberWorkLoop.jsto match elements between renders, avoiding unnecessary unmounting. - State preservation: Stable keys ensure that
useState,useRef, and other local state survive list reordering operations. - Performance optimization: Proper key assignment minimizes DOM mutations by allowing React to move existing nodes rather than deleting and recreating them.
- Data-derived identifiers: Use database IDs or other stable primitives; avoid object references and array indices unless the list is completely static.
- Sibling uniqueness: Keys must be unique within the array but do not require global uniqueness across the component tree.
Frequently Asked Questions
What happens if I use array index as a key in React?
Using array indices as keys causes React to use index-based reconciliation, which works correctly only for static lists that never change order, length, or content. If you insert, delete, or reorder items, React will mismatch components, causing state loss, unnecessary re-renders, and potential bugs with controlled inputs or animations.
Can I use objects or arrays as keys in React?
No, you should not use objects or arrays as keys. React internally converts keys to strings using String(key), which means objects become the string "[object Object]", destroying uniqueness. According to the type definitions in the React source, valid key types are string | number | null.
Do keys need to be globally unique or just unique among siblings?
Keys only need to be unique among siblings within the same array. You can use the same key values in different lists or different component hierarchies without conflict. The reconciler compares keys only within the scope of the parent component's children.
How do keys affect component state during reordering?
Keys determine component identity across renders. When a list is reordered, React matches elements by their key values. If an element maintains the same key, React recognizes it as the same instance and preserves its internal state (including hooks like useState and useRef). Without stable keys, React treats the element as a new instance, resetting all local state.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →