Keys & List Rendering
Understand how React renders lists, why keys are the single most important prop for list performance, and how wrong keys cause some of the most confusing bugs in React applications.
Table of Contents
Overview
Lists are everywhere in UIs — feeds, tables, search results, navigation menus. React renders lists by mapping data to elements with .map(), but it needs a way to track which items changed, moved, or were removed between renders. That's what keys do.
Keys are a special string prop that gives each list element a stable identity. During reconciliation, React uses keys to match old elements with new ones. With good keys, React reuses existing DOM nodes and only updates what changed. With bad keys (or none), React may destroy and recreate elements unnecessarily — causing performance issues, lost component state, and subtle UI bugs.
This topic is deceptively simple on the surface but deep enough to be a recurring interview question. The bugs caused by wrong keys are some of the hardest to debug in React.
Why this matters
"Why shouldn't you use index as a key?" is asked in nearly every React interview. But beyond interviews, wrong keys cause real production bugs — inputs showing the wrong data, animations breaking, and components losing state. Understanding keys deeply prevents all of these.
Rendering Lists in React
React doesn't have a built-in loop construct for rendering lists. Instead, you use JavaScript's .map() to transform an array of data into an array of JSX elements.
function FruitList() { const fruits = ["Apple", "Banana", "Cherry"]; return ( <ul> {fruits.map((fruit) => ( <li key={fruit}>{fruit}</li> ))} </ul> ); } // React creates three <li> elements: // <li>Apple</li> // <li>Banana</li> // <li>Cherry</li>
What Happens When the List Changes?
Lists are dynamic — items get added, removed, reordered, or updated. When the array changes and the component re-renders, React needs to figure out what happened to each element.
Item Added
A new element appears in the array. React needs to create a new DOM node and insert it at the right position.
Item Removed
An element disappears from the array. React needs to find the corresponding DOM node and remove it.
Item Reordered
Elements change position. React needs to move existing DOM nodes instead of destroying and recreating them.
// Render 1: ["Apple", "Banana", "Cherry"] <ul> <li>Apple</li> <li>Banana</li> <li>Cherry</li> </ul> // Render 2: ["Mango", "Apple", "Banana", "Cherry"] // (Mango added at the beginning) <ul> <li>Mango</li> // ← new <li>Apple</li> // ← moved from position 0 to 1 <li>Banana</li> // ← moved from position 1 to 2 <li>Cherry</li> // ← moved from position 2 to 3 </ul> // How does React know Apple "moved" vs "changed to Mango"? // Without keys, it can't. It compares by position. // With keys, it matches by identity.
The fundamental problem
Arrays are ordered by index, but list items have identity. "Apple" is still "Apple" whether it's at index 0 or index 1. Keys bridge this gap — they tell React which item is which, regardless of position.
What Are Keys?
A key is a special string attribute you give to elements inside a .map() call. It's not a regular prop — React uses it internally during reconciliation and strips it before passing props to the component.
// Keys go on the outermost element returned by .map() function UserList({ users }) { return ( <ul> {users.map((user) => ( // key goes HERE — on the <li>, not inside UserCard <li key={user.id}> <UserCard user={user} /> </li> ))} </ul> ); } // If the component IS the outermost element: function UserList({ users }) { return ( <div> {users.map((user) => ( // key goes on the component itself <UserCard key={user.id} user={user} /> ))} </div> ); } // Keys are NOT passed as props to the component: function UserCard({ user }) { // There is no props.key here — React consumes it internally return <div>{user.name}</div>; }
Key Rules
- →Keys must be unique among siblings — two items in the same list can't share a key
- →Keys must be stable — the same item should always get the same key across renders
- →Keys should be strings — React converts numbers to strings internally, but strings are preferred
- →Keys only need to be unique within their list — different lists can reuse the same keys
- →Keys are not passed as props — use a different prop name if the child needs the ID
What happens without keys?
If you don't provide keys, React uses the array index by default and shows a warning in the console. Index keys work fine for static lists that never change order. They break badly for dynamic lists — we'll see exactly how in the next sections.
How React Uses Keys (Reconciliation Deep Dive)
During reconciliation, React compares the old list of children with the new list. Keys are the mechanism it uses to match old elements to new ones. The behavior is dramatically different with and without proper keys.
Without Keys: Compare by Position
// Without keys (or with index keys), React compares by position // Old list: New list (Mango added at start): // index 0: <li>Apple</li> index 0: <li>Mango</li> // index 1: <li>Banana</li> index 1: <li>Apple</li> // index 2: <li>Cherry</li> index 2: <li>Banana</li> // index 3: <li>Cherry</li> // React's diff (comparing by index): // index 0: "Apple" → "Mango" → UPDATE text content // index 1: "Banana" → "Apple" → UPDATE text content // index 2: "Cherry" → "Banana" → UPDATE text content // index 3: (nothing) → "Cherry" → CREATE new <li> // Result: 3 text updates + 1 creation = 4 DOM operations // React updated EVERY existing element even though // Apple, Banana, Cherry didn't change — they just moved.
With Keys: Compare by Identity
// With unique keys, React matches by identity // Old list: New list: // key="apple": <li>Apple</li> key="mango": <li>Mango</li> // key="banana": <li>Banana</li> key="apple": <li>Apple</li> // key="cherry": <li>Cherry</li> key="banana": <li>Banana</li> // key="cherry": <li>Cherry</li> // React's diff (matching by key): // key="mango": no old match → CREATE new <li>Mango</li> // key="apple": found in old list → MOVE (reuse DOM node) // key="banana": found in old list → MOVE (reuse DOM node) // key="cherry": found in old list → MOVE (reuse DOM node) // Result: 1 creation + 3 moves = much cheaper // Existing DOM nodes are REUSED, not recreated. // Component state inside Apple/Banana/Cherry is PRESERVED.
The Internal Algorithm
Step 1: Build a Map from old children oldMap = { "apple" → <li>Apple</li> (fiber + DOM node), "banana" → <li>Banana</li> (fiber + DOM node), "cherry" → <li>Cherry</li> (fiber + DOM node), } Step 2: Walk through new children in order for each newChild: look up newChild.key in oldMap │ ├─ FOUND → Reuse the old fiber + DOM node │ Compare props → update if changed │ Remove from oldMap (it's been matched) │ └─ NOT FOUND → Create new fiber + DOM node Step 3: Delete unmatched old children anything left in oldMap was removed from the list → unmount component, remove DOM node Step 4: Commit position changes move DOM nodes to match the new order
Build a lookup map of old children by key
React creates a Map<key, fiber> from the previous render's children. This allows O(1) lookups when matching new children.
Match new children against the map
For each child in the new list, React looks up its key in the map. If found, the old fiber and DOM node are reused. If not found, a new element is created.
Delete unmatched old children
Any old children whose keys don't appear in the new list are unmounted — their effects are cleaned up and DOM nodes are removed.
Apply position changes
React moves reused DOM nodes to their new positions in a single batch. This is much cheaper than destroying and recreating elements.
Why this is O(n)
Building the map is O(n). Looking up each new child is O(1) per child, so O(n) total. Deleting leftovers is O(remaining). The whole process is linear — React never compares every old child against every new child (which would be O(n²)). Keys make this possible.
Why Keys Matter
Keys aren't just a performance optimization — they're a correctness mechanism. Wrong keys don't just make things slow; they cause bugs that are extremely hard to track down.
DOM Reuse
With correct keys, React reuses existing DOM nodes when items move. Without them, React destroys and recreates nodes — expensive and causes visual flicker.
State Preservation
Component state (useState, useRef, input values) is tied to the key. Correct keys keep state with the right item. Wrong keys attach state to the wrong item.
Effect Cleanup
When a key disappears, React unmounts the component and runs cleanup effects. When a key appears, React mounts fresh. This controls the component lifecycle.
Bug: State Attached to the Wrong Item
// ❌ BUG: Using index as key with stateful components function EmailList() { const [emails, setEmails] = useState([ { id: "e1", from: "Alice", subject: "Meeting tomorrow" }, { id: "e2", from: "Bob", subject: "Code review" }, { id: "e3", from: "Carol", subject: "Lunch plans" }, ]); const deleteEmail = (id) => { setEmails(emails.filter(e => e.id !== id)); }; return ( <ul> {emails.map((email, index) => ( // ❌ Using index as key <EmailRow key={index} email={email} onDelete={deleteEmail} /> ))} </ul> ); } function EmailRow({ email, onDelete }) { // Each row has an "expanded" state (user clicked to read) const [isExpanded, setIsExpanded] = useState(false); return ( <li> <div onClick={() => setIsExpanded(!isExpanded)}> {email.from}: {email.subject} <button onClick={() => onDelete(email.id)}>Delete</button> </div> {isExpanded && <div className="body">Full email content...</div>} </li> ); } // Scenario: User expands Alice's email (index 0), then deletes it. // // Before delete: // key=0: Alice (expanded=true) ← user expanded this // key=1: Bob (expanded=false) // key=2: Carol (expanded=false) // // After delete (Alice removed): // key=0: Bob (expanded=true!) ← BUG! Bob inherits Alice's state // key=1: Carol (expanded=false) // // React sees: key=0 still exists, props changed (Alice → Bob). // It UPDATES the component but keeps the STATE (expanded=true). // Bob's email is now expanded even though the user never clicked it.
// ✅ FIX: Using unique ID as key <ul> {emails.map((email) => ( // ✅ Stable, unique key <EmailRow key={email.id} email={email} onDelete={deleteEmail} /> ))} </ul> // After deleting Alice (id="e1"): // key="e1": REMOVED → React unmounts, cleans up state // key="e2": Bob (expanded=false) → unchanged, correct ✓ // key="e3": Carol (expanded=false) → unchanged, correct ✓ // // React sees: key="e1" is gone → delete it. // key="e2" and key="e3" still exist → reuse, no state change. // Bob keeps his own state. No bug.
The core insight
Keys don't just affect performance — they determine which component instance receives which data. Wrong keys mean wrong state. This is why the React docs call keys a "hint" to the reconciler about element identity, not just an optimization.
Good vs Bad Keys
Not all keys are created equal. The quality of your keys directly determines whether React can efficiently reconcile your lists or falls back to expensive, buggy behavior.
| Key Strategy | Example | Stable? | Unique? | Verdict |
|---|---|---|---|---|
| Database ID | key={user.id} | ✅ | ✅ | Best choice |
| UUID / nanoid | key={item.uuid} | ✅ | ✅ | Great |
| Unique natural value | key={email.address} | ✅ | ✅ | Good |
| Composite key | key={`${type}-${id}`} | ✅ | ✅ | Good |
| Array index | key={index} | ❌ | ⚠️ | Dangerous |
| Math.random() | key={Math.random()} | ❌ | ✅ | Worst |
Why Index Keys Break
// Index keys are tied to POSITION, not IDENTITY // Before: ["Apple", "Banana", "Cherry"] // key=0 → Apple // key=1 → Banana // key=2 → Cherry // After removing "Apple": ["Banana", "Cherry"] // key=0 → Banana (was Apple!) // key=1 → Cherry (was Banana!) // (key=2 removed — that was Cherry's key!) // React thinks: // - key=0 changed content (Apple → Banana) → UPDATE // - key=1 changed content (Banana → Cherry) → UPDATE // - key=2 disappeared → DELETE // // Reality: // - Apple was deleted // - Banana and Cherry didn't change at all // // React did 2 unnecessary updates + deleted the wrong element. // If these had state (inputs, checkboxes), the state is now wrong.
Why Math.random() Is Even Worse
// ❌ NEVER do this {items.map(item => ( <ListItem key={Math.random()} item={item} /> ))} // Every render generates NEW keys for EVERY item. // React sees: all old keys gone, all new keys are fresh. // Result: DESTROY every component and CREATE new ones. // // On every single render: // - All component state is lost // - All DOM nodes are recreated // - All effects run cleanup + setup // - All animations restart // - All input focus is lost // // This is literally the worst possible performance — // worse than no keys at all.
When Index Keys Are OK
// Index keys are safe ONLY when ALL three conditions are true: // 1. The list is static (never reordered, filtered, or sorted) // 2. Items are never added to or removed from the middle // 3. Items have no local state (no inputs, no checkboxes, no expanded state) // ✅ Safe: static navigation menu const navItems = ["Home", "About", "Contact"]; navItems.map((item, index) => ( <NavLink key={index}>{item}</NavLink> )); // ✅ Safe: static table headers const headers = ["Name", "Email", "Role"]; headers.map((header, index) => ( <th key={index}>{header}</th> )); // ❌ NOT safe: dynamic todo list (items added/removed) // ❌ NOT safe: sortable table rows // ❌ NOT safe: any list with inputs or interactive elements // Rule of thumb: if in doubt, use a unique ID. Always safe.
Generating keys when you don't have IDs
If your data doesn't have unique IDs, generate them when the data is created — not during render. Use crypto.randomUUID() or a counter when items are added to the array. The key must be stable across renders, so it must be part of the data, not computed in the render function.
Real-World Bug Walkthrough
Let's walk through a realistic scenario step by step to see exactly how wrong keys cause bugs and how correct keys fix them.
The Scenario: Editable Task List
A task list where each item has a text label and an input field for notes. Users can add tasks at the top and delete any task.
// ❌ BUGGY: Using index as key function TaskList() { const [tasks, setTasks] = useState([ { id: "t1", name: "Design mockups" }, { id: "t2", name: "Write API docs" }, { id: "t3", name: "Fix login bug" }, ]); const addTask = () => { const newTask = { id: crypto.randomUUID(), name: "New task" }; setTasks([newTask, ...tasks]); // Add to BEGINNING }; const deleteTask = (id) => { setTasks(tasks.filter(t => t.id !== id)); }; return ( <div> <button onClick={addTask}>Add Task</button> <ul> {tasks.map((task, index) => ( <TaskRow key={index} task={task} onDelete={deleteTask} /> ))} </ul> </div> ); } function TaskRow({ task, onDelete }) { return ( <li> <span>{task.name}</span> <input placeholder="Add notes..." /> <button onClick={() => onDelete(task.id)}>×</button> </li> ); }
The Bug in Action
Step 1: Initial render key=0: "Design mockups" [input: empty] key=1: "Write API docs" [input: empty] key=2: "Fix login bug" [input: empty] Step 2: User types "need by Friday" in the first input key=0: "Design mockups" [input: "need by Friday"] ← user typed here key=1: "Write API docs" [input: empty] key=2: "Fix login bug" [input: empty] Step 3: User clicks "Add Task" (adds to beginning) NEW array: ["New task", "Design mockups", "Write API docs", "Fix login bug"] React compares by index: key=0: "Design mockups" → "New task" → UPDATE text input state stays at key=0! key=1: "Write API docs" → "Design mockups" → UPDATE text key=2: "Fix login bug" → "Write API docs" → UPDATE text key=3: (new) → "Fix login bug" → CREATE Result on screen: key=0: "New task" [input: "need by Friday"] ← BUG! Wrong input! key=1: "Design mockups" [input: empty] key=2: "Write API docs" [input: empty] key=3: "Fix login bug" [input: empty] The note "need by Friday" is now on "New task" instead of "Design mockups"! The user's data is attached to the wrong item. 😱
The Fix: Unique IDs
// ✅ FIXED: Using unique ID as key <ul> {tasks.map((task) => ( <TaskRow key={task.id} task={task} onDelete={deleteTask} /> ))} </ul>
Step 1: Initial render key="t1": "Design mockups" [input: empty] key="t2": "Write API docs" [input: empty] key="t3": "Fix login bug" [input: empty] Step 2: User types "need by Friday" in the first input key="t1": "Design mockups" [input: "need by Friday"] key="t2": "Write API docs" [input: empty] key="t3": "Fix login bug" [input: empty] Step 3: User clicks "Add Task" (adds to beginning) NEW array: [newTask, t1, t2, t3] React matches by key: key="new-uuid": no match → CREATE new <li> key="t1": found in old list → REUSE (move to position 1) key="t2": found in old list → REUSE (move to position 2) key="t3": found in old list → REUSE (move to position 3) Result on screen: key="new-uuid": "New task" [input: empty] ← correct! key="t1": "Design mockups" [input: "need by Friday"] ← correct! key="t2": "Write API docs" [input: empty] ← correct! key="t3": "Fix login bug" [input: empty] ← correct! Every input stays with its task. State is preserved correctly. ✓
This bug is extremely common
This exact bug shows up in production apps all the time — especially in forms, tables with inline editing, and drag-and-drop lists. It's hard to catch in testing because it only manifests when items have local state AND the list order changes. The fix is always the same: use unique, stable keys.
Performance Insights
Keys are the foundation of list rendering performance. But for truly large lists, keys alone aren't enough — you need additional strategies.
Unique Keys Enable DOM Reuse
With proper keys, React moves existing DOM nodes instead of destroying and recreating them. For a list of 1,000 items where one is added, that's 1 creation vs 1,000 updates.
Combine Keys with React.memo
Keys identify WHICH items changed. React.memo prevents re-rendering items whose props didn't change. Together, only truly changed items re-render.
Stable Keys Prevent Unnecessary Unmount/Mount Cycles
Unstable keys (Math.random, Date.now) force React to unmount and remount every item on every render. This destroys state, reruns effects, and recreates DOM — the worst case.
Virtualize Large Lists
For lists with 1,000+ items, even perfect keys can't help if all items are in the DOM. Use react-window or react-virtuoso to render only visible items.
Use the Key Reset Pattern Intentionally
Changing a key on purpose forces React to unmount and remount a component — resetting all state. Useful for forms that need to reset when switching between items.
Performance Comparison
| Operation | Index Keys | Unique Keys | Random Keys |
|---|---|---|---|
| Add item at end | 1 create | 1 create | N destroy + N create |
| Add item at start | N updates + 1 create | 1 create + N moves | N destroy + N create |
| Remove item from middle | N updates + 1 delete | 1 delete | N destroy + N create |
| Reorder items | N updates | N moves (cheap) | N destroy + N create |
| State preservation | Wrong item gets state | Correct ✓ | All state lost |
DOM moves are cheap
Moving a DOM node (insertBefore, appendChild) is significantly cheaper than creating a new one. A new element requires allocating memory, setting up event listeners, and triggering layout. A move just changes the node's position in the tree. This is why key-based reconciliation is so much faster.
Common Mistakes
These mistakes appear in codebases and interviews constantly. Recognizing them instantly shows you understand React internals, not just the API.
Using index as key in dynamic lists
Index keys work for static lists but break when items are added, removed, or reordered. React matches by index position, so state gets attached to the wrong items and DOM updates are inefficient.
✅Use a unique, stable identifier from your data (database ID, UUID). If your data doesn't have IDs, generate them when items are created — not during render.
Not providing keys at all
Omitting keys triggers a React console warning and falls back to index-based comparison. This has the same problems as explicit index keys, plus it signals to other developers that keys weren't considered.
✅Always provide explicit keys on elements returned from .map(). Even if index is acceptable (static list), being explicit shows intent.
Generating keys during render
Using Math.random(), Date.now(), or crypto.randomUUID() inside .map() creates new keys every render. React thinks every item is new — it destroys and recreates the entire list on every update.
✅Generate IDs when data is created (in event handlers, API responses, or initial state). Keys must be stable across renders — the same item must always produce the same key.
Using non-unique keys
If two siblings share the same key, React can't distinguish them. It may reuse the wrong component instance, leading to state corruption and rendering glitches. React warns about this in development.
✅Keys must be unique among siblings. If your data has duplicate values, combine fields to create a unique key: key={`${item.type}-${item.id}`}.
Putting the key on the wrong element
The key must go on the outermost element returned by .map() — not on an inner element. Putting it on a child element means the parent has no key, and React falls back to index comparison.
✅Always put key on the element directly inside .map(): items.map(item => <Card key={item.id} ... />). If you wrap in a Fragment, use <Fragment key={item.id}>.
Expecting key to be available as a prop
Developers try to access props.key inside a component. React consumes the key internally and doesn't pass it as a prop. Trying to read it returns undefined.
✅If the child component needs the ID, pass it as a separate prop: <Item key={item.id} id={item.id} />. Use a different prop name like 'id' or 'itemId'.
Interview Questions
Keys come up in almost every React interview. These questions range from basic definitions to deep reconciliation understanding.
Q:What are keys in React and why are they needed?
A: Keys are special string props that give list elements a stable identity. During reconciliation, React uses keys to match old elements with new ones. Without keys, React compares children by position (index), which leads to unnecessary DOM updates and state bugs when items are reordered, added, or removed. Keys enable React to reuse existing DOM nodes and preserve component state correctly.
Q:Why shouldn't you use array index as a key?
A: Index keys tie identity to position, not to the data. When items are reordered, added at the beginning, or removed from the middle, the index-to-item mapping changes. React thinks existing items changed content (because the index now points to different data) instead of recognizing that items moved. This causes unnecessary DOM updates and, critically, attaches component state (inputs, checkboxes) to the wrong items.
Q:What happens internally when React encounters a list with keys?
A: React builds a Map of old children keyed by their key prop. For each child in the new list, it looks up the key in the map. If found, it reuses the old fiber and DOM node (updating props if needed). If not found, it creates a new element. Any old keys not present in the new list are deleted (unmounted). Finally, React moves DOM nodes to match the new order. This is O(n) thanks to the hash map lookup.
Q:When is it safe to use index as a key?
A: Only when all three conditions are true: (1) the list is completely static — never reordered, filtered, or sorted, (2) items are never added to or removed from the middle, and (3) list items have no local state (no inputs, checkboxes, or expanded/collapsed state). In practice, this means only hardcoded lists like navigation menus or table headers. For any dynamic data, use unique IDs.
Q:What happens if you use Math.random() as a key?
A: Every render generates new random keys for every item. React sees all old keys as gone and all new keys as fresh — it unmounts every component and mounts new ones from scratch. This is the worst possible performance: all state is lost, all DOM nodes are recreated, all effects run cleanup and setup, all animations restart, and all input focus is lost. It's worse than no keys at all.
Q:How do keys relate to component state?
A: React ties component state to the key + position in the tree. When a key persists across renders, React reuses the component instance and preserves its state. When a key disappears, React unmounts the component and destroys its state. When a new key appears, React mounts a fresh instance. This is why wrong keys cause state bugs — the state follows the key, not the data.
Q:What is the 'key reset pattern' and when would you use it?
A: Changing a component's key forces React to unmount the old instance and mount a fresh one, resetting all internal state. This is useful when you want to reset a form when switching between items: <EditForm key={selectedId} item={selectedItem} />. When selectedId changes, the form unmounts and remounts with fresh state — no manual reset logic needed.
Q:Can two elements in different lists have the same key?
A: Yes. Keys only need to be unique among siblings — elements in the same .map() call. Different lists, different components, or different parts of the tree can freely reuse the same key values. React only compares keys within the same parent's children array.
Practice Section
Work through these scenarios to build intuition for key-related bugs and solutions. Try to answer before reading the solution.
Why Is This List Behaving Incorrectly?
A sortable table uses index as key. When the user clicks 'Sort by Name,' the table rows update their text but the checkbox states (checked/unchecked) stay in their original positions. Row 1 was checked before sorting, and after sorting, whatever row ends up in position 1 appears checked — even though it's a different item. Why?
Answer: Index keys tie state to position, not identity. When the list is sorted, the items move to new positions but the keys (indices) stay the same. React sees key=0 still exists and keeps its component state (checked=true), but the data at that position changed. The checkbox state follows the index, not the item. Fix: use item.id as the key so React moves the component instance (with its state) along with the data.
What Key Should Be Used Here?
You're rendering a list of chat messages. Each message has: { id, text, timestamp, userId }. Some messages can be edited (text changes) and messages can be deleted. What should the key be?
Answer: Use message.id as the key. It's unique (each message has its own ID), stable (the ID doesn't change when the message is edited), and comes from the data source. Don't use timestamp (two messages could have the same timestamp) or index (messages can be deleted). Don't use text (it changes when edited, which would cause React to unmount and remount the component).
What Happens When Items Reorder?
A drag-and-drop list has 5 items with proper unique keys. The user drags item 3 to position 1. How many DOM operations does React perform? What about with index keys?
Answer: With unique keys: React moves 1 DOM node (item 3 to position 1). The other 4 items stay in place or shift naturally. All component state is preserved. With index keys: React updates the text content of positions 1, 2, and 3 (3 DOM text updates) because the data at those indices changed. If items have state (inputs, checkboxes), that state is now attached to the wrong items.
Debugging a Key Warning
React shows: "Warning: Each child in a list should have a unique 'key' prop." The developer adds key={item.name} but some items share the same name. Now React shows: "Warning: Encountered two children with the same key." What should they do?
Answer: Keys must be unique among siblings. If item.name isn't unique, it can't be used as a key. Options: (1) Use a unique field like item.id if available. (2) Create a composite key: key={`${item.name}-${item.category}`} if the combination is unique. (3) If no unique field exists, generate a stable ID when the data is created: items.map(item => ({ ...item, _id: crypto.randomUUID() })) — do this once when data is loaded, not on every render.
The Key Reset Pattern
You have a UserProfile component that fetches user data and has internal state (active tab, scroll position). When the user navigates from /user/1 to /user/2, the component shows user 2's data but the tab and scroll position from user 1 are still active. How do you fix this without adding useEffect cleanup?
Answer: Add a key based on the user ID: <UserProfile key={userId} user={user} />. When userId changes from 1 to 2, the key changes, so React unmounts the old UserProfile (clearing all state) and mounts a fresh one. The new instance starts with default state — first tab selected, scroll at top. This is the 'key reset pattern' — using React's reconciliation behavior to reset state without manual cleanup.
Cheat Sheet (Quick Revision)
One-screen summary for quick revision before interviews.
Quick Revision Cheat Sheet
What keys do: Give list elements a stable identity so React can match old and new items during reconciliation.
Without keys: React compares by index position. Reordering/inserting causes unnecessary updates and state bugs.
With keys: React builds a Map of old children by key. Matches new children in O(1) per item. Reuses DOM nodes.
Best key: Unique, stable ID from your data — database ID, UUID, or unique natural value.
Index as key: Only safe for static, never-reordered lists with no component state. Avoid for dynamic lists.
Math.random() as key: Worst option. New keys every render = destroy and recreate entire list. All state lost.
Key rules: Must be unique among siblings. Must be stable across renders. Must be a string.
Key placement: Goes on the outermost element inside .map(). Not on inner children.
Key as prop: React consumes key internally. Pass ID separately if child needs it: <Item key={id} id={id} />.
State follows key: Component state is tied to the key. Same key = preserved state. New key = fresh state.
Key reset pattern: Change key to force unmount/remount. Resets all internal state without manual cleanup.
Generate IDs: Create IDs when data is created (event handler, API response). Never generate in render.
Performance: Good keys: O(n) with DOM reuse. Index keys: O(n) with unnecessary updates. Random keys: O(n) full rebuild.