Keys in React
What a key is for, why the index is usually wrong, and what breaks without one.
What you will be able to do
- Explain what React does with a key when a list re-renders
- Reproduce the index-key bug, and explain exactly why it happens
- Explain why random keys lose all state, even on an unrelated re-render
- Choose a good key - and know where it should come from
- Say when an index key is genuinely safe
- Use a key on purpose to reset a component
The idea, in plain English
Every time a component re-renders, React compares the new list of elements with the previous one and decides which existing components to keep, which to create, and which to throw away. Components that are kept keep their state - what was typed into an input, whether a checkbox is ticked, anything held in useState.
Without keys, React matches items by position: the first new item is the first old item, and so on. A key replaces position with identity. Item with key 3 is the item that had key 3 last time, wherever it now sits in the list.
The examples use useState, which Module 3 covers properly. For now you only need to know that it holds a value that survives re-renders - and belongs to one component instance, which is exactly what a key decides.
That makes the choice of key a choice about where state goes. A key that follows the data - an id - keeps each item state attached to its item. A key that follows the position - the index - keeps state attached to the slot, so when items move, their state stays behind. A key that changes every render - Math.random() - means nothing ever matches, and all state is thrown away.
Worked example: Reordering a list and watching the input values follow the wrong row.
What React does with a key
After a re-render, React walks the new list. For each element, it looks for an element in the old list with the same key and the same type. If it finds one, it reuses that component - its state, its DOM nodes - and just updates its props. If it finds none, it creates a new component. Any old key missing from the new list is removed, state and all.
This comparison is part of reconciliation, the process React uses to work out the smallest change to the DOM. Keys are how you tell it which item is which.
Keys are compared only among siblings - the children of the same parent. Two different lists can both use keys 1, 2, and 3 without any conflict.
The index-key bug, reproduced
Take a list of two rows - React and Node.js - where each row has a notes input and a checkbox held in state. Type "my notes on React" into the first row and tick its box. Then insert TypeScript at the top.
With key={item.id}, React sees ids 3, 1, 2: id 3 is new, and ids 1 and 2 are the rows it already has, now further down. The notes and the tick stay on React, where they belong.
With key={index}, React sees keys 0, 1, 2. Key 0 existed before, so it reuses the component at key 0 - the one holding your notes and tick - and just gives it the new title, TypeScript. React itself gets key 1, which used to be Node.js, and arrives with an empty input. Your notes are now on the wrong course. There is no warning, because nothing is technically wrong: every key is unique.
Every number in that description comes from running it in React 19. The same thing happens when you remove an item, sort, or filter - anything that changes which item is at which index.
Watch out: The index-key bug shows up only when items hold state - an input, a checkbox, an open/closed toggle, a component with useState. A list of plain text looks fine with index keys, which is exactly why the bug survives until the list gets interactive.
Random keys: nothing ever matches
key={Math.random()} produces a new key on every render, so no new key ever matches an old one. React throws away every item and creates it again, every time the list renders.
That is not only slow. Type into an input in such a list, then trigger any re-render - even one that has nothing to do with the list - and the text vanishes, because the input was destroyed and replaced. The same happens with crypto.randomUUID() or Date.now() called during render.
Duplicate keys
Two siblings with the same key break the matching: React logs "Encountered two children with the same key" and may duplicate or drop items when the list changes.
Duplicates usually mean the key is not really unique in the data - titles, names, or ids that come from two different tables and happen to collide. Combine fields if needed: key={module.id + "-" + lesson.id}.
Where a good key comes from
The best key is an id the data already has: a database id, a slug, an email, an order number. It is stable across renders and unique among siblings, which is all a key needs.
If items are created in the browser and have no id yet, give them one when they are created - const newTodo = { id: crypto.randomUUID(), text }; - and store it with the item. Generating an id is fine; generating it during render is the bug.
A key does not need to be a number, or globally unique, or secret. It needs to be the same for the same item on every render, and different from its siblings.
item.id from the dataBest. Stable and unique.A slug or natural keyGood, if it is unique among siblings and never edited.An id created with the itemGood - generate it when the item is created, store it with the item.The indexOnly for lists that never reorder, insert, or remove, and hold no state.Math.random() / randomUUID() in renderNever. Every render remounts everything.A title or nameRisky - two items can share one, and editing it remounts the item.Watch out: crypto.randomUUID() exists only in secure contexts - HTTPS pages and localhost. Open your Vite dev server from a phone at http://192.168.x.x and it is undefined. Test on localhost, or use a small id helper that does not depend on it.
When the index is fine
An index key is safe when the list is static - it never reorders, and items are never inserted or removed except at the end - and the items hold no state of their own. A fixed set of tabs, the lines of a poem, the columns of a table header.
Appending is safe too: adding an item at the end does not change any existing index. The trouble always starts at the front or the middle.
If an id is available, use it anyway. It costs nothing, and the list will not break the day someone adds sorting.
Keys outside lists: resetting a component
A key works on any element, not only inside map. Changing the key of a single component tells React it is a different component: the old one is removed, a new one is created, and its state starts fresh.
That is the standard way to reset a form when the thing it edits changes. <ProfileEditor key={userId} user={user} /> gives each user a fresh editor. Without the key, switching from one user to another keeps the draft typed for the first.
Syntax and examples
import { useState } from "react";
type Course = { id: number; title: string };
function Row({ title }: { title: string }) {
const [done, setDone] = useState(false);
return (
<li>
<input placeholder={title + " notes"} />
<label>
<input type="checkbox" checked={done} onChange={() => setDone(!done)} />
{title}
</label>
</li>
);
}
function CourseNotes() {
const [courses, setCourses] = useState<Course[]>([
{ id: 1, title: "React" },
{ id: 2, title: "Node.js" },
]);
function addToTop() {
setCourses([{ id: 3, title: "TypeScript" }, ...courses]);
}
return (
<>
<button onClick={addToTop}>Add TypeScript to the top</button>
<ul>
{courses.map((course, index) => (
<Row key={index} title={course.title} /> // try key={course.id}
))}
</ul>
</>
);
}Before: type "my notes on React" in the first row and tick it.
key={index} key={course.id}
TypeScript | notes: "my notes on React" | ✓ TypeScript | notes: "" |
React | notes: "" | React | notes: "my notes on React" | ✓
Node.js | notes: "" | Node.js | notes: "" |
key={Math.random()}
TypeScript | notes: "" |
React | notes: "" | every row recreated - all state lost
Node.js | notes: "" |key={index}
before: 0 -> React (state: notes, ✓) 1 -> Node.js
after: 0 -> TypeScript 1 -> React 2 -> Node.js
React reuses the component at key 0 - with its state - for TypeScript.
key={course.id}
before: 1 -> React (state: notes, ✓) 2 -> Node.js
after: 3 -> TypeScript (new) 1 -> React 2 -> Node.js
Key 1 is still React, so the state stays with React.function Search({ tags }: { tags: string[] }) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Clicked {count}</button>
<ul>
{tags.map((tag) => (
<li key={Math.random()}>
<input placeholder={tag} />
</li>
))}
</ul>
</>
);
}
// Type into an input, then click the button.
// The button has nothing to do with the list - the text is gone anyway.const courses = [
{ id: 1, title: "React" },
{ id: 1, title: "Node.js" },
];
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>;
// Console (development):
// Encountered two children with the same key ... Keys should be unique so that
// components maintain their identity across updates.type Todo = { id: string; text: string };
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [text, setText] = useState("");
function add() {
// The id is created once, with the item, and stored with it.
setTodos([...todos, { id: crypto.randomUUID(), text }]);
setText("");
}
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={add}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}// Both lists use keys 1 and 2 - no conflict, they have different parents.
<>
<ul>
{frontend.map((c) => <li key={c.id}>{c.title}</li>)}
</ul>
<ul>
{backend.map((c) => <li key={c.id}>{c.title}</li>)}
</ul>
</>;
// Two sources in one list: combine them to stay unique.
{[...courses, ...paths].map((item) => (
<li key={item.type + "-" + item.id}>{item.title}</li>
))}function DraftEditor({ name }: { name: string }) {
const [draft, setDraft] = useState("");
return (
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder={"Note for " + name}
/>
);
}
function Notes({ user }: { user: string }) {
return (
<>
{/* Keeps the old draft when user changes */}
<DraftEditor name={user} />
{/* Starts empty for each user */}
<DraftEditor key={user} name={user} />
</>
);
}// A fixed list: never reordered, never filtered, no state in the items.
const TABS = ["Overview", "Lessons", "Reviews"];
<nav>
{TABS.map((tab, index) => (
<a key={index} href={"#" + tab.toLowerCase()}>
{tab}
</a>
))}
</nav>;
// Still, key={tab} works here and survives the day someone reorders the tabs.Tip: To see keys at work, render a list whose rows contain an input, type into one, and then reorder, insert at the top, or remove. Whatever React does with your typing is what it thinks the key means.
Watch out: React warns about missing keys and duplicate keys - but not about index keys or random keys. Those are valid keys that mean the wrong thing, so you have to catch them yourself.
Key rules
StableThe same item gets the same key on every render.
key={course.id}Unique among siblingsNot globally - only within one parent.
key={type + "-" + id}On the outermost elementThe element or component map returns.
<Card key={c.id} />Not a propThe component never receives it.
props.key === undefined
Changing it remountsNew key, new component, fresh state.
<Editor key={userId} />Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Build the CourseNotes example with key={index}. Type into the React row, tick it, and click "Add TypeScript to the top". Where did your notes go?”
“Change the key to course.id and repeat. Then try removing the first course instead of adding one.”
“Use key={Math.random()}, type into a row, and click any button that re-renders the component.”
“Render <DraftEditor name={user} /> with and without key={user}, type a draft, and switch the user. Which one keeps the draft?”
What usually goes wrong
State stays with the position, so it lands on the wrong item after an insert, remove, sort, or filter - with no warning.
✗ {courses.map((course, index) => <Row key={index} course={course} />)}✓ {courses.map((course) => <Row key={course.id} course={course} />)}A new key every render means every item is destroyed and recreated, losing all state and focus.
✗ <li key={Math.random()}>✓ <li key={item.id}>crypto.randomUUID() inside map is just as random as Math.random(). Create the id once, with the item.
✗ {todos.map((t) => <li key={crypto.randomUUID()}>{t.text}</li>)}✓ setTodos([...todos, { id: crypto.randomUUID(), text }]);Duplicate keys trigger a warning and can duplicate or drop items when the list updates.
✗ {lessons.map((l) => <li key={l.title}>{l.title}</li>)} // two lessons called "Introduction"✓ {lessons.map((l) => <li key={l.id}>{l.title}</li>)}If the key is a title and the user renames the item, the key changes, and React remounts the item - losing its state mid-edit.
✗ <CourseEditor key={course.title} />✓ <CourseEditor key={course.id} />Best practices
- Use an id from the data as the key whenever one exists.
- Create ids for new client-side items once, when they are created.
- Reserve index keys for static lists without state - and prefer an id even then.
- Never generate keys during render.
- Combine fields when a list mixes items from different sources.
- Use a changing key deliberately to reset a component.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
A list of three course rows, each with a notes input, uses key={index}. The user types notes on the second row and then deletes the first row. Which row shows the notes afterwards, and why?
Show hintHide hint
After the delete, which index does the second course have - and which component used to have that index?
Show solutionHide solution
Before: index 0 -> A, index 1 -> B (notes typed here), index 2 -> C
After: index 0 -> B, index 1 -> C
React reuses the component at key 1 - the one holding the notes -
for whatever is now at index 1, which is C.
The notes typed for B now appear on C. With key={course.id},
the component for B keeps its key and the notes stay on B.Pick a key for each list: (a) orders from an API with an orderId, (b) todos created in the browser, (c) the three fixed tabs Overview, Lessons, Reviews, (d) search results mixing courses and learning paths, each with its own numeric id.
Show hintHide hint
Stable, unique among siblings, and never generated during render.
Show solutionHide solution
(a) key={order.orderId}
(b) key={todo.id}, with id: crypto.randomUUID() set when the todo is created
(c) key={tab} (the index would also be safe - the list never changes)
(d) key={result.type + "-" + result.id} (a course and a path can share an id)A ProfileEditor keeps a draft in state. When the page switches from one user to another, the old draft stays in the form. Fix it without changing ProfileEditor.
Show hintHide hint
A new key means a new component.
Show solutionHide solution
<ProfileEditor key={user.id} user={user} />A reorderable lesson list that keeps its notes
Build a list of lessons with a notes input on each row and buttons to move rows, and prove that the notes follow their lessons.
- Start with three lessons, each { id, title }.
- Each row shows the title, a notes input, and Up and Down buttons.
- Up and Down swap the row with its neighbour.
- Type notes into two rows, move them around, and check the notes stay with their lessons.
- Switch the key to the index and describe what goes wrong.
import { useState } from "react";
type Lesson = { id: number; title: string };
const initial: Lesson[] = [
{ id: 1, title: "JSX Fundamentals" },
{ id: 2, title: "Conditional Rendering" },
{ id: 3, title: "Rendering Lists" },
];
function LessonOrder() {
const [lessons, setLessons] = useState(initial);
function move(from: number, to: number) {
// swap lessons[from] and lessons[to]
}
// render the rows
}Show one solutionHide solution
import { useState } from "react";
type Lesson = { id: number; title: string };
const initial: Lesson[] = [
{ id: 1, title: "JSX Fundamentals" },
{ id: 2, title: "Conditional Rendering" },
{ id: 3, title: "Rendering Lists" },
];
function LessonOrder() {
const [lessons, setLessons] = useState(initial);
function move(from: number, to: number) {
if (to < 0 || to >= lessons.length) return;
const next = [...lessons];
[next[from], next[to]] = [next[to], next[from]];
setLessons(next);
}
return (
<ol>
{lessons.map((lesson, index) => (
<li key={lesson.id}>
{lesson.title} <input placeholder="Notes" />
<button onClick={() => move(index, index - 1)}>Up</button>
<button onClick={() => move(index, index + 1)}>Down</button>
</li>
))}
</ol>
);
}
// With key={lesson.id}, each input moves with its lesson.
// With key={index}, the titles move but the inputs stay in place -
// the notes end up next to a different lesson.Key points
- React matches old and new list items by key; without keys, by position.
- Same key and type: the component is reused and keeps its state.
- Index keys tie state to the position, so it lands on the wrong item after an insert, remove, or sort.
- Random keys match nothing, so every render recreates every item and all state is lost.
- React warns about missing and duplicate keys, not about index or random ones.
- Use an id from the data; create ids for new items once, when they are created.
- Keys only need to be unique among siblings.
- Changing a component key resets it - useful on purpose.
Quick check before you move on
Interview questions
What is a key and why does React need one?
A key is the identity of an element among its siblings. During reconciliation React matches old and new children by key and type, reusing components that match - with their state - and creating or removing the rest.
Why are array indexes bad keys?
An index describes a position, not an item. When items are inserted, removed, or reordered, the same index points at a different item, so React reuses the wrong component and its state - input text, checkboxes, useState values - ends up on the wrong item, without any warning.
When is an index key acceptable?
For a static list that never changes order, never has items inserted or removed except at the end, and whose items hold no state. Even then a stable id is safer.
Why not use Math.random() as a key?
It changes every render, so no key ever matches. React unmounts and remounts every item each time, which loses state and focus and wastes work.
What makes a good key?
Stable across renders and unique among siblings - usually an id from the data. Client-created items should get an id when they are created and keep it.
How can you use a key outside a list?
To reset a component. Changing its key makes React treat it as a new component, so its state starts fresh - for example key={userId} on a form that edits the current user.
Quiz
- 1.
What does React use a key for? A. Styling list items B. Identifying each item across renders C. Sorting the list D. Passing an id to the component
- 2.
Rows hold input state and use key={index}. You insert an item at the top. What happens? A. Nothing unusual B. React warns about the key C. The typed values stay in place and now sit beside different items D. All inputs are cleared
- 3.
What happens to state in a list with key={Math.random()} when the parent re-renders? A. It is kept B. It is lost, because every item is recreated C. It moves to the next item D. React throws
- 4.
Two different ul elements both render items with keys 1, 2, and 3. Is that a problem? A. Yes - keys must be globally unique B. No - keys only need to be unique among siblings
- 5.
What does changing the key on <Editor key={userId} /> do? A. Nothing B. Passes userId as a prop C. Creates a new Editor with fresh state D. Throws a warning
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned