EdgeCases Logo
Feb 2026
React
Expert
8 min read

React Key Prop: The Reconciliation Deep Dive

Keys aren't just for warning suppression—they control whether React preserves or destroys component instances. Misuse causes invisible state bugs and performance problems.

react
keys
reconciliation
diffing
state-management
performance
edge-case

The key prop isn't just about avoiding console warnings. It's React's identity system—the mechanism that determines whether a component instance is preserved or destroyed. Get it wrong, and you'll face invisible bugs: forms that don't reset, animations that fire unexpectedly, and stale state that persists when it shouldn't.

What React Sees Without Keys

// Before
<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

// After (Carrot prepended)
<ul>
  <li>Carrot</li>
  <li>Apple</li>
  <li>Banana</li>
</ul>

Without keys, React compares by position. It sees:

  • Position 0: "Apple" → "Carrot" (update text)
  • Position 1: "Banana" → "Apple" (update text)
  • Position 2: (new) → "Banana" (insert)

Three DOM mutations. If the li elements had internal state (like an expanded accordion or input focus), that state would now be misaligned— "Apple's" state is now attached to "Carrot's" DOM node.

How Keys Fix This

// With keys
<ul>
  {items.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

React now compares by key, not position:

  • key="apple": same → no update
  • key="banana": same → no update
  • key="carrot": new → insert at position 0

One DOM mutation. State attached to "apple" and "banana" components remains correctly associated.

The Reconciliation Algorithm

React's diffing uses a two-pass approach for children:

// Simplified reconciliation pseudocode
function reconcileChildren(oldChildren, newChildren) {
  const oldKeyMap = new Map();
  
  // Pass 1: Build map of old children by key
  oldChildren.forEach((child, index) => {
    const key = child.key ?? index; // Fallback to index if no key
    oldKeyMap.set(key, { child, index });
  });
  
  // Pass 2: Match new children to old by key
  newChildren.forEach((newChild, newIndex) => {
    const key = newChild.key ?? newIndex;
    const old = oldKeyMap.get(key);
    
    if (old) {
      // Same key found: reuse fiber, update props
      updateFiber(old.child, newChild);
      oldKeyMap.delete(key);
    } else {
      // No match: create new fiber
      createFiber(newChild);
    }
  });
  
  // Remaining old children with no matching key: delete
  oldKeyMap.forEach(({ child }) => deleteFiber(child));
}

State Preservation vs State Reset

Keys control whether React preserves or destroys component instances:

function App() {
  const [isAdmin, setIsAdmin] = useState(false);
  
  return (
    <div>
      {isAdmin ? (
        <UserProfile user={adminUser} />  // Same component type
      ) : (
        <UserProfile user={regularUser} />  // Same position
      )}
    </div>
  );
}

When isAdmin toggles, UserProfile is at the same position with the same type—React reuses the instance. Any local state (like an open modal or form input) persists, even though the user changed!

Force Reset with Key

// ✅ Key forces new instance when user changes
{isAdmin ? (
  <UserProfile key="admin" user={adminUser} />
) : (
  <UserProfile key="regular" user={regularUser} />
)}

Now toggling isAdmin destroys the old UserProfile and creates a new one. State resets as expected.

The Index Key Anti-Pattern

// ❌ Using array index as key
{items.map((item, index) => (
  <TodoItem key={index} item={item} />
))}

This is functionally identical to having no key at all. When items reorder:

// Before: [A, B, C] → keys [0, 1, 2]
// After:  [B, A, C] → keys [0, 1, 2]

// React sees:
// key=0: A → B (update)
// key=1: B → A (update)
// key=2: C → C (no change)

If TodoItem has internal state (checked, editing, etc.), that state stays at position 0 even though item A moved to position 1.

When Index Keys Are Acceptable

  • Static lists that never reorder, filter, or insert
  • Items with no internal state (pure display)
  • Items with no stable unique identifier
// ✅ Static navigation - order never changes
{navItems.map((item, index) => (
  <NavLink key={index} to={item.path}>{item.label}</NavLink>
))}

// ✅ Display-only list - no state to preserve
{tags.map((tag, index) => (
  <span key={index} className="tag">{tag}</span>
))}

Composite Keys for Nested Data

When rendering nested structures, combine parent and child identifiers:

// ❌ Child IDs might collide across parents
{categories.map((category) => (
  <section key={category.id}>
    {category.items.map((item) => (
      <Item key={item.id} />  // item.id=1 in multiple categories
    ))}
  </section>
))}

// ✅ Composite key ensures uniqueness
{categories.map((category) => (
  <section key={category.id}>
    {category.items.map((item) => (
      <Item key={`${category.id}-${item.id}`} />
    ))}
  </section>
))}

Keys Across Component Boundaries

Keys only matter among siblings—they're not globally unique:

// These keys don't conflict
<div>
  <Header key="header" />
  <Sidebar key="header" />  {/* Different parent = OK */}
</div>

// But these do - both are children of the same fragment
<>
  <Header key="main" />
  <Content key="main" />  {/* ❌ Duplicate key among siblings */}
</>

Debugging Key-Related Bugs

Signs that keys are misconfigured:

  • Form inputs retain values after list items reorder
  • Animations trigger on items that didn't change
  • Focus jumps unexpectedly when list updates
  • useEffect cleanup doesn't run when expected
  • Refs point to wrong DOM nodes after reorder
// Debug component instance identity
function DebugItem({ id }) {
  const instanceId = useRef(Math.random().toString(36).slice(2));
  
  console.log(`Render: id=${id}, instance=${instanceId.current}`);
  
  useEffect(() => {
    console.log(`Mount: id=${id}, instance=${instanceId.current}`);
    return () => console.log(`Unmount: instance=${instanceId.current}`);
  }, []);
  
  return <div>{id}</div>;
}

If the instance ID changes when it shouldn't (or doesn't change when it should), your keys are wrong.

Performance Implications

Poor key choices hurt performance in two ways:

  1. Unnecessary updates: Without stable keys, React updates every item even if only one changed
  2. Lost optimization opportunities: React.memo, useMemo, and useCallback compare props, but a remounted component runs all initialization code regardless
// With index keys, adding an item at the start
// causes EVERY item to re-render with new props

// With stable keys, only the new item mounts
// existing items bail out via memo

Key Takeaways

  • Keys tell React which component instance to reuse vs recreate
  • Same key + same position = state preserved
  • Different key at same position = state destroyed, fresh instance
  • Index keys only work for static, stateless lists
  • Use key intentionally to force component reset when needed
  • Composite keys prevent collisions in nested structures

Advertisement

Related Insights

Explore related edge cases and patterns

React
Deep
React Concurrent Rendering Tearing: When External Stores Break
7 min
React
Deep
React useId: Why Math.random() Breaks Hydration
7 min
CSS
Surface
CSS Anchor Positioning: Tooltips Without JavaScript
6 min

Advertisement