Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props

2.2.e — Keys in React

In one sentence: Keys are special string attributes that help React identify which items in a list have changed, been added, or removed, enabling efficient updates instead of re-rendering the entire list.

Navigation: ← 2.2.d — Rendering Lists · Next → 2.2.f — Reusable Card Component


Table of Contents

  1. Why React Needs Keys
  2. How Reconciliation Works
  3. What Makes a Good Key
  4. The Index-as-Key Problem
  5. Proper Key Strategies
  6. Keys and Component State
  7. Keys to Force Re-Mount
  8. Keys in Fragments
  9. Composite Keys
  10. Debugging Key Warnings
  11. Performance Impact — With vs Without Keys
  12. Key Rules Summary
  13. Common Mistakes
  14. Key Takeaways
  15. Explain-It Challenge

1. Why React Needs Keys

When React re-renders a list, it needs to figure out what changed. Without keys, React has no way to match old items to new items — it can only compare them by position.

┌─────────────────────────────────────────────────────────────┐
│              WITHOUT KEYS — Position-Based Matching          │
│                                                             │
│  Before:         After (Alice removed):                     │
│  ┌──────────┐    ┌──────────┐                               │
│  │ 0: Alice │    │ 0: Bob   │  ← React thinks Alice         │
│  ├──────────┤    ├──────────┤     changed TO Bob             │
│  │ 1: Bob   │    │ 1: Charlie│  ← React thinks Bob          │
│  ├──────────┤    └──────────┘     changed TO Charlie         │
│  │ 2: Charlie│                  ← React thinks Charlie       │
│  └──────────┘                      was DELETED               │
│                                                             │
│  Result: React updates 2 items + deletes 1                  │
│  Reality: Only Alice was removed                            │
│  Wasted work: 2 unnecessary DOM updates                     │
│                                                             │
│─────────────────────────────────────────────────────────────│
│              WITH KEYS — Identity-Based Matching             │
│                                                             │
│  Before:              After (Alice removed):                │
│  ┌──────────────┐     ┌──────────────┐                      │
│  │ key=1: Alice │     │ key=2: Bob   │  ← Same key,         │
│  ├──────────────┤     ├──────────────┤     no update needed  │
│  │ key=2: Bob   │     │ key=3: Charlie│  ← Same key,        │
│  ├──────────────┤     └──────────────┘     no update needed  │
│  │ key=3: Charlie│                                           │
│  └──────────────┘                      ← key=1 is gone,     │
│                                           just remove it    │
│                                                             │
│  Result: React removes 1 DOM node (Alice), moves nothing    │
│  Optimal!                                                   │
└─────────────────────────────────────────────────────────────┘

2. How Reconciliation Works

React's reconciliation algorithm (the "diffing" process) handles lists by comparing children at each position:

Without Keys — O(n) Updates

Step 1: Compare position 0
  Old: <li>Alice</li>
  New: <li>Bob</li>
  → Different! Update DOM text node from "Alice" to "Bob"

Step 2: Compare position 1
  Old: <li>Bob</li>
  New: <li>Charlie</li>
  → Different! Update DOM text node from "Bob" to "Charlie"

Step 3: Compare position 2
  Old: <li>Charlie</li>
  New: (nothing)
  → Remove DOM node

Total: 3 DOM operations (2 updates + 1 deletion)

With Keys — O(1) for Matching

Step 1: Build key map from old list
  { key=1: Alice, key=2: Bob, key=3: Charlie }

Step 2: Walk new list, match by key
  key=2 (Bob)    → Found in old list, same position? No → move it
  key=3 (Charlie) → Found in old list, same position? No → move it

Step 3: Remove unmatched old keys
  key=1 (Alice)  → Not in new list → remove DOM node

Total: 1 DOM operation (1 deletion, 0 content updates)

The Algorithm Visualized

// React's simplified list reconciliation:
function reconcileList(oldChildren, newChildren) {
  const oldKeyMap = {};
  
  // Phase 1: Index old children by key
  oldChildren.forEach((child, i) => {
    const key = child.key || i;  // Fall back to index if no key
    oldKeyMap[key] = { element: child, index: i };
  });
  
  // Phase 2: Match new children to old by key
  newChildren.forEach((child, i) => {
    const key = child.key || i;
    const oldMatch = oldKeyMap[key];
    
    if (oldMatch) {
      // Key exists in both — update if props changed
      if (propsChanged(oldMatch.element, child)) {
        updateDOM(oldMatch.element, child);
      }
      delete oldKeyMap[key];
    } else {
      // New key — insert new DOM node
      insertDOM(child, i);
    }
  });
  
  // Phase 3: Remove old keys not in new list
  Object.values(oldKeyMap).forEach(({ element }) => {
    removeDOM(element);
  });
}

3. What Makes a Good Key

Key Requirements

RequirementDescriptionExample
Unique among siblingsNo two items in the same list can share a keykey={user.id} (IDs are unique)
StableSame item always gets the same key, across re-rendersDatabase ID ✅, Math.random()
Not the indexUnless the list is static and never reorderskey={index} is fragile

Good Keys

// Database ID — best choice
users.map(user => <UserCard key={user.id} user={user} />);

// UUID / nanoid — generated once and stored
items.map(item => <ListItem key={item.uuid} item={item} />);

// Unique natural identifier
countries.map(country => <Option key={country.code} value={country.code}>{country.name}</Option>);
// "US", "UK", "JP" — naturally unique

// Email (if unique in the dataset)
contacts.map(contact => <ContactCard key={contact.email} contact={contact} />);

// Slug (URL-friendly identifier)
blogPosts.map(post => <PostCard key={post.slug} post={post} />);

Bad Keys

// ❌ Math.random() — different on every render! Destroys all performance.
items.map(item => <div key={Math.random()}>{item.name}</div>);

// ❌ Date.now() — same problem, regenerated on each render
items.map(item => <div key={Date.now()}>{item.name}</div>);

// ❌ Index when items can reorder/insert/delete
items.map((item, i) => <div key={i}>{item.name}</div>);

// ❌ Non-unique values (multiple items could share the same name)
items.map(item => <div key={item.name}>{item.name}</div>);

4. The Index-as-Key Problem

Using array indices as keys is the most common mistake. Here's exactly why it breaks.

The Bug: Stateful Components Get Wrong State

function App() {
  const [items, setItems] = useState([
    { id: 1, text: "Buy milk" },
    { id: 2, text: "Walk dog" },
    { id: 3, text: "Read book" },
  ]);

  const removeFirst = () => {
    setItems(prev => prev.slice(1));  // Remove first item
  };

  return (
    <div>
      <button onClick={removeFirst}>Remove First</button>
      {items.map((item, index) => (
        // ❌ Using index as key
        <TodoItem key={index} text={item.text} />
      ))}
    </div>
  );
}

function TodoItem({ text }) {
  const [checked, setChecked] = useState(false);

  return (
    <div>
      <input 
        type="checkbox" 
        checked={checked} 
        onChange={() => setChecked(!checked)} 
      />
      {text}
    </div>
  );
}

What happens when you check "Buy milk" then click "Remove First":

BEFORE (user checks "Buy milk"):
  index=0: "Buy milk"    ☑ (checked=true)
  index=1: "Walk dog"    ☐ (checked=false)
  index=2: "Read book"   ☐ (checked=false)

AFTER (remove first item):
  index=0: "Walk dog"    ☑ (checked=true)  ← BUG! This was "Buy milk"'s state!
  index=1: "Read book"   ☐ (checked=false)

React sees:
  key=0 still exists → reuse component → keep state (checked=true)
  key=1 still exists → reuse component → keep state (checked=false)
  key=2 disappeared → destroy component

But the text changed from "Buy milk" to "Walk dog", so now
"Walk dog" is checked even though the user never checked it!

Fix: Use Stable IDs

{items.map(item => (
  // ✅ key matches the DATA, not the position
  <TodoItem key={item.id} text={item.text} />
))}

Now when "Buy milk" (id=1) is removed:

  • React sees key=1 is gone → destroys that component and its state
  • key=2 ("Walk dog") and key=3 ("Read book") keep their correct states

When Index IS Acceptable

ScenarioIndex OK?Why
Static list, never changesPositions are stable
Display-only, no stateNo state to mix up
No reordering/insertion/deletionIndices stay consistent
Items have no unique ID⚠️ Last resortBetter to generate IDs
Items can be reorderedState will be wrong
Items can be inserted/deletedState will be wrong
Items have input fields or checkboxesUser input gets mixed up

5. Proper Key Strategies

Strategy 1: Database IDs

// Most common — every database record has a unique ID
function UserList({ users }) {
  return users.map(user => (
    <UserCard key={user.id} user={user} />
  ));
}
// user.id could be: 1, 2, 3 (integer) or "abc123" (string)

Strategy 2: Generate IDs on Creation

import { nanoid } from "nanoid";

function TodoApp() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos(prev => [
      ...prev,
      { 
        id: nanoid(),  // Generate ID when creating the item
        text, 
        done: false 
      },
    ]);
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Strategy 3: Natural Unique Identifiers

// Country codes
countries.map(c => <Option key={c.iso3166} ... />);

// File paths
files.map(f => <FileItem key={f.path} ... />);

// URLs
links.map(l => <LinkCard key={l.url} ... />);

// Usernames (if guaranteed unique)
members.map(m => <MemberBadge key={m.username} ... />);

Strategy 4: Crypto.randomUUID() (Modern Browsers)

const addItem = (text) => {
  setItems(prev => [
    ...prev,
    { id: crypto.randomUUID(), text },
    // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
  ]);
};

6. Keys and Component State

Keys have a critical relationship with component state: when a key changes, React destroys the old component and creates a new one from scratch.

Same Key = Preserve State

function App() {
  const [user, setUser] = useState({ id: 1, name: "Alice" });

  return (
    <div>
      {/* Same key=1 across renders → state is preserved */}
      <EditForm key={user.id} user={user} />
      <button onClick={() => setUser({ id: 1, name: "Alice Updated" })}>
        Update Name
      </button>
    </div>
  );
}

function EditForm({ user }) {
  const [draft, setDraft] = useState(user.name);
  // If key stays 1, draft state is KEPT even when user prop changes
  // This is actually a bug! (See "Keys to Force Re-Mount" section)
  return <input value={draft} onChange={e => setDraft(e.target.value)} />;
}

Different Key = Destroy + Recreate

function App() {
  const [userId, setUserId] = useState(1);
  const users = { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } };

  return (
    <div>
      {/* When userId changes 1→2, key changes, component is DESTROYED and RECREATED */}
      <EditForm key={userId} user={users[userId]} />
      <button onClick={() => setUserId(userId === 1 ? 2 : 1)}>
        Switch User
      </button>
    </div>
  );
}

function EditForm({ user }) {
  const [draft, setDraft] = useState(user.name);
  // When key changes, this component is freshly mounted
  // draft is re-initialized from user.name — correct behavior!
  return <input value={draft} onChange={e => setDraft(e.target.value)} />;
}

7. Keys to Force Re-Mount

This is a powerful technique: change a component's key to force it to reset.

Problem: Form Doesn't Reset When Switching Items

// ❌ BUG: Switching between users doesn't reset the form
function UserEditor({ selectedUserId, users }) {
  const user = users.find(u => u.id === selectedUserId);
  
  return <EditForm user={user} />;
  // When selectedUserId changes from 1 to 2,
  // React sees the same <EditForm> component at the same position
  // → it UPDATES props but KEEPS state
  // → the form still shows user 1's draft text!
}

// ✅ FIX: Add key={selectedUserId} to force re-mount
function UserEditor({ selectedUserId, users }) {
  const user = users.find(u => u.id === selectedUserId);
  
  return <EditForm key={selectedUserId} user={user} />;
  // When selectedUserId changes, key changes
  // React DESTROYS old EditForm, creates a new one
  // All state is fresh — draft is re-initialized from new user
}

Common Use Cases for Key-Based Reset

// Reset form when editing a different item
<EditForm key={item.id} item={item} />

// Reset animation when content changes
<FadeIn key={slideIndex}>
  <SlideContent slide={slides[slideIndex]} />
</FadeIn>

// Reset timer when game restarts
<GameTimer key={gameId} duration={60} />

// Reset scroll position when navigating
<PageContent key={currentRoute} route={currentRoute} />

8. Keys in Fragments

When you need to return multiple elements without a wrapper, and each needs a key:

function DefinitionList({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        // Can't use <></> shorthand with keys
        // Must use <React.Fragment key={...}>
        <React.Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

// ❌ This won't work — shorthand fragments can't have keys
{terms.map(term => (
  <key={term.id}>   // Syntax error!
    <dt>{term.word}</dt>
    <dd>{term.definition}</dd>
  </>
))}

// Alternative: import Fragment
import { Fragment } from "react";

{terms.map(term => (
  <Fragment key={term.id}>
    <dt>{term.word}</dt>
    <dd>{term.definition}</dd>
  </Fragment>
))}

9. Composite Keys

Sometimes no single field is unique, but a combination is.

// Schedule: same room can have different events on different days
// Neither eventName nor room nor day is unique alone
const schedule = [
  { day: "Monday", room: "A101", event: "Math" },
  { day: "Monday", room: "A102", event: "Science" },
  { day: "Tuesday", room: "A101", event: "English" },
  { day: "Tuesday", room: "A101", event: "History" },  // Same room, same day!
];

// ❌ No single field is unique
schedule.map(item => (
  <ScheduleItem key={item.day} ... />  // Duplicate keys!
));

// ✅ Composite key
schedule.map((item, index) => (
  <ScheduleItem key={`${item.day}-${item.room}-${index}`} item={item} />
));

// ✅ Even better — use a delimiter that won't appear in data
schedule.map(item => (
  <ScheduleItem key={`${item.day}|${item.room}|${item.event}`} item={item} />
));

Generating Composite Keys Safely

function makeKey(...parts) {
  return parts.map(p => String(p)).join("::");
}

// Usage
schedule.map(item => (
  <ScheduleItem 
    key={makeKey(item.day, item.room, item.timeSlot)} 
    item={item} 
  />
));

10. Debugging Key Warnings

The Warning

Warning: Each child in a list should have a unique "key" prop.

Common Causes and Fixes

// CAUSE 1: Forgot to add key
// ❌
items.map(item => <div>{item.name}</div>);
// ✅
items.map(item => <div key={item.id}>{item.name}</div>);

// CAUSE 2: Key on wrong element
// ❌ Key is on the inner element, not the outermost
items.map(item => (
  <div>
    <span key={item.id}>{item.name}</span>
  </div>
));
// ✅ Key must be on the outermost element returned by map
items.map(item => (
  <div key={item.id}>
    <span>{item.name}</span>
  </div>
));

// CAUSE 3: Duplicate keys
const items = [
  { id: 1, name: "Apple" },
  { id: 1, name: "Banana" },  // Same id!
];
// Warning: Encountered two children with the same key "1"
// Fix: Ensure IDs are actually unique in your data

// CAUSE 4: Key inside the child component
// ❌ Key can't be accessed inside the component
function ListItem({ key, item }) {
  // key is NOT accessible as a prop!
  return <div>{item.name}</div>;
}
// ✅ If you need the same value inside, pass it as a separate prop
function ListItem({ id, item }) {
  return <div data-id={id}>{item.name}</div>;
}
items.map(item => <ListItem key={item.id} id={item.id} item={item} />);

Key Is NOT a Prop

function Child({ key }) {
  console.log(key); // undefined! React consumes key, doesn't pass it
}

<Child key="abc" />
// key="abc" is used by React internally, NOT available in the component

// If you need the same value as a prop, pass it separately:
<Child key={item.id} id={item.id} />

11. Performance Impact — With vs Without Keys

Benchmark Scenario: Remove First Item from a List of 1,000

┌─────────────────────────────────────────────────────────────┐
│        PERFORMANCE: Removing First Item from 1000           │
│                                                             │
│  WITHOUT proper keys (index as key):                        │
│  ├── Item 0: text "A" → text "B"     UPDATE                │
│  ├── Item 1: text "B" → text "C"     UPDATE                │
│  ├── Item 2: text "C" → text "D"     UPDATE                │
│  ├── ...                                                    │
│  ├── Item 998: text "YYY" → text "ZZZ"  UPDATE             │
│  └── Item 999: removed              DELETE                  │
│  Total: 999 text updates + 1 deletion = 1000 operations    │
│                                                             │
│  WITH proper keys (unique ID):                              │
│  └── key="id_1": removed             DELETE                 │
│  Total: 1 deletion = 1 operation                            │
│                                                             │
│  Speedup: ~1000x for this operation!                        │
└─────────────────────────────────────────────────────────────┘

When It Matters Most

ScenarioImpact of Bad Keys
Small static list (5 items)Negligible
Large list (100+ items) with reorderingSevere — hundreds of unnecessary DOM updates
List items with input fieldsCritical — user data gets mixed between items
List items with animationsVisible bugs — animations trigger on wrong items
List items with fetch-on-mountSevere — unnecessary API calls when items "change"
Drag-and-drop listsBroken — items jump to wrong positions

12. Key Rules Summary

┌─────────────────────────────────────────────────────────────┐
│                    KEY RULES CHEAT SHEET                      │
│                                                             │
│  1. ALWAYS add keys to list items                           │
│     items.map(item => <X key={item.id} />)                  │
│                                                             │
│  2. Keys must be UNIQUE among siblings                      │
│     (Different lists can reuse the same keys)               │
│                                                             │
│  3. Keys must be STABLE across renders                      │
│     ✅ Database IDs, UUIDs, natural identifiers             │
│     ❌ Math.random(), Date.now()                            │
│                                                             │
│  4. Use index ONLY for static, display-only lists           │
│                                                             │
│  5. Key goes on the OUTERMOST element in map()              │
│                                                             │
│  6. Key is NOT accessible as a prop inside the component    │
│                                                             │
│  7. Change a key to FORCE component re-mount (reset state)  │
│                                                             │
│  8. Use <React.Fragment key={...}> when mapping              │
│     to multiple elements                                    │
│                                                             │
│  9. Composite keys: `${field1}-${field2}` when no           │
│     single field is unique                                  │
│                                                             │
│  10. Generate IDs at creation time, not render time          │
└─────────────────────────────────────────────────────────────┘

13. Common Mistakes

Mistake 1: Generating Keys During Render

// ❌ New key every render — component is destroyed and recreated each time!
items.map(item => <Card key={Math.random()} item={item} />);
// This is WORSE than no key at all — forces React to destroy and rebuild
// every component on every render. Animations break. Input values lost.
// State lost. Performance destroyed.

// ❌ Same problem with crypto.randomUUID() in render
items.map(item => <Card key={crypto.randomUUID()} item={item} />);

// ✅ Use a stable identifier that was set when the item was CREATED
items.map(item => <Card key={item.id} item={item} />);

Mistake 2: Non-Unique Keys

// ❌ Multiple users could share the same name
users.map(user => <li key={user.name}>{user.name}</li>);
// If two users are named "John", React only renders one of them

// ✅ Use a guaranteed unique field
users.map(user => <li key={user.id}>{user.name}</li>);

Mistake 3: Keys on the Wrong Element

// ❌ Key on inner span, not on the li
items.map(item => (
  <li>
    <span key={item.id}>{item.name}</span>
  </li>
));

// ✅ Key on the outermost element
items.map(item => (
  <li key={item.id}>
    <span>{item.name}</span>
  </li>
));

Mistake 4: Thinking Keys Affect Rendering Order

// Keys do NOT control display order
// This will still render in array order: Charlie, Alice, Bob
const items = [
  { id: 3, name: "Charlie" },
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

items.map(item => <li key={item.id}>{item.name}</li>);
// Renders: Charlie, Alice, Bob (array order, NOT key order)

// To change order, sort the ARRAY first:
[...items]
  .sort((a, b) => a.name.localeCompare(b.name))
  .map(item => <li key={item.id}>{item.name}</li>);
// Renders: Alice, Bob, Charlie

14. Key Takeaways

  1. Keys let React track identity — which items are the same across renders
  2. Without keys, React uses position — causing unnecessary DOM updates and state bugs
  3. Use stable, unique identifiers — database IDs, UUIDs, natural keys (email, slug, code)
  4. Never use Math.random() or Date.now() — these change every render, destroying performance
  5. Index is acceptable ONLY for static, non-reorderable, stateless lists
  6. The index-as-key bug: stateful components get each other's state when items shift position
  7. Changing a key forces re-mount — useful for resetting forms, animations, timers
  8. Keys are consumed by React — they're not accessible as props inside the component
  9. Use <React.Fragment key={...}> when mapping to multiple sibling elements
  10. Generate IDs when creating data, not during rendering

Explain-It Challenge

  1. The Classroom: Imagine a teacher with 30 students. On day 1, she learns names (keys). On day 2, one student transfers out. With keys (names), she knows exactly who left. Without keys (only seat positions), she thinks 29 students changed seats. How does this relate to React's reconciliation?

  2. The Hotel: A hotel has rooms (DOM nodes) and guests (components). When a guest checks out (item removed from list), the hotel knows which room to clean by room number (key). Without room numbers, the hotel would have to check every room. Explain how this maps to React's key-based diffing.

  3. The Bug Report: Your colleague shows you this code and says "there's a bug — when I delete a todo, the checkboxes get mixed up." Walk through exactly why key={index} causes this and how key={todo.id} fixes it.

todos.map((todo, index) => (
  <TodoItem key={index} text={todo.text} />
))

Navigation: ← 2.2.d — Rendering Lists · Next → 2.2.f — Reusable Card Component