Keys in the First List
You've probably already written something like this by now:
lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));And if you learned React from tutorials, chances are somebody told you, "always add a key when using map", you added it, warning disappeared, and then you moved on.
Fair enough.
But what exactly is that key doing there? Why does React complain when we don't have one? And why do people keep saying don't use array indexes, even though key={index} makes the warning disappear just fine?
The reason is that arrays can change between renders.
React renders one list, then later your data changes and React renders another list. Now React has to work out which item from the old list corresponds to which item in the new list.
That's what the key helps React with.
lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));Here lesson.id tells React which lesson this LessonItem represents among its siblings. If that lesson moves somewhere else in the array later, the ID still stays with the same lesson, so React can match it properly.
Let's actually see why we care.
What happens when you don't have keys
Say we have three lessons:
position 0 -> Expressions
position 1 -> Attributes
position 2 -> FragmentsNow we insert another lesson at the beginning:
position 0 -> JSX Overview
position 1 -> Expressions
position 2 -> Attributes
position 3 -> FragmentsWithout proper keys, React has much less information about which old item belongs to which new item.
At position 0, there used to be Expressions. Now there's JSX Overview.
At position 1, there used to be Attributes. Now there's Expressions.
And so on.
If every item is just plain text, this can still look totally fine on screen. React can update the text and you may never notice anything weird happened.
But list items usually aren't just text.
Maybe every LessonItem has some local state. Maybe there's an <input> inside it. Maybe one item currently has focus. Maybe an Effect is running for that item. Now the difference between "this is the same lesson moved to another position" and "this is some other lesson now sitting at this position" becomes pretty important.
With proper keys, React sees something more useful:
jsx-overview -> JSX Overview
expressions -> Expressions
attributes -> Attributes
fragments -> Fragmentsexpressions still means Expressions even though it moved from position 0 to position 1.
That's what we want.
A key should stay attached to the same data record while that record exists.
Use the ID your data already has
Usually your data already gives you the answer.
const lessons = [
{
id: "lesson_01J6Q6X7Y8",
title: "Keys in the First List",
},
];Then use that ID:
<LessonItem key={lesson.id} lesson={lesson} />Pretty boring, yes. Also usually correct.
If your records use some permanent slug instead, that can work too:
<LessonItem key={lesson.slug} lesson={lesson} />But only if that slug really stays the same for that record.
Suppose you use the lesson title:
<LessonItem key={lesson.title} lesson={lesson} />Maybe that works today.
Then somebody renames "React Basics" to "React Fundamentals".
Now the key changed too. React sees the old key disappear and a new key appear, so from React's point of view that's a different item instance.
The lesson didn't become a different lesson. Only its title changed. So title wasn't a very good ID for it.
Usually the best key is whatever ID your database, API, or local data model already uses for that record.
Random IDs are fine... just not during render
Suppose you're creating lessons locally and there is no backend ID yet.
You can generate one when the lesson gets created:
const newLesson = {
id: crypto.randomUUID(),
title: "Untitled lesson",
};Then store that object in your state or whatever data store you're using.
Later renders keep reading the same ID:
<LessonItem key={lesson.id} lesson={lesson} />That's fine.
Now look at this:
lessons.map((lesson) => (
<LessonItem
key={crypto.randomUUID()}
lesson={lesson}
/>
));Looks reasonable at first, doesn't it?
Every item gets a unique key. What more does React want from us?
Well, the problem is those keys change on every render.
First render maybe one lesson gets:
f3c1...Next render the exact same lesson gets:
a972...React has no reason to connect those two elements anymore. The old key disappeared and a brand new key showed up.
So React treats it as a new item.
And you'll actually notice that if the component has local state or some DOM state inside it. Inputs can reset, focus can disappear, component state starts fresh, DOM nodes can get replaced, and Effects for the old instance can clean up before the new instance runs its own Effects.
So yeah, crypto.randomUUID() itself isn't the problem. Calling it while rendering existing records is.
Generate the ID when you create the record, save it with the record, and then keep using that same ID.
What about key={index}?
This one comes up a lot:
lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} />
));And technically React stops complaining, so it can feel like we're done.
But index tells React the item's current position. It doesn't tell React which record this actually is.
Say Expressions is currently at index 0:
0 -> Expressions
1 -> Attributes
2 -> FragmentsInsert JSX Overview at the start:
0 -> JSX Overview
1 -> Expressions
2 -> Attributes
3 -> FragmentsExpressions used to have key 0.
Now it has key 1.
Same lesson, different key.
And JSX Overview now gets key 0, which used to belong to Expressions.
You can probably already see the problem.
Insertions aren't the only case either. Delete an item and later indexes move. Sort the list and indexes move. Filter it and positions can change again.
Can index keys ever be okay? Yeah, in a list that genuinely never changes order, never gets items inserted in between, never gets items removed, and doesn't depend on preserving item state across those changes.
That's a lot of conditions though.
If you already have lesson.id, just use it:
<LessonItem key={lesson.id} lesson={lesson} />Also don't do this:
key={`${lesson.id}-${index}`}It looks safer because the ID is there, but adding the index means the whole key still changes when the item moves.
If lesson.id already identifies the lesson, adding position information gives you nothing useful.
Keys only need to be unique among siblings
A React key does not need to be globally unique across your entire application.
Say you have two separate lists:
function PublishedList({ lessons }) {
return lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));
}And somewhere else:
function DraftList({ lessons }) {
return lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));
}Both lists can contain a lesson with key "lesson-1".
That's okay.
React compares keys among siblings under the same parent. The "lesson-1" inside PublishedList isn't competing with "lesson-1" inside DraftList.
But two siblings in the same returned list should not have the same key:
lesson-1 -> first item
lesson-1 -> second itemNow React can't reliably use that key to tell those siblings apart.
You'll get a warning, and updates can behave incorrectly.
And no, adding some random value during render to make the warning disappear doesn't fix the data. If two records are supposed to represent different things, they need IDs that actually identify them as different records.
Put the key on the thing you're mapping
This is another easy mistake.
Suppose your mapping creates LessonItem components:
lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));That's correct.
Now maybe you think, "I'll just put the key inside LessonItem instead."
function LessonItem({ lesson }) {
return <li key={lesson.id}>{lesson.title}</li>;
}Nope.
The parent is producing a list of LessonItem elements. Those are the siblings React is trying to match at that level, and those LessonItem elements still have no keys.
By the time React gets to the li inside the component, we're already inside another component render.
So keep the component itself normal:
function LessonItem({ lesson }) {
return <li>{lesson.title}</li>;
}And put the key where the array is being created:
lessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
));A decent question to ask yourself is: what element is this map() directly returning?
Put the key there.
What if one item returns multiple elements?
Sometimes one record needs to produce more than one sibling DOM element.
For example:
terms.map((term) => (
<>
<dt>{term.name}</dt>
<dd>{term.definition}</dd>
</>
));We need one key for the whole pair because both elements belong to the same term.
But the short fragment syntax <>...</> doesn't let us pass a key.
So use Fragment directly:
import { Fragment } from "react";Then:
terms.map((term) => (
<Fragment key={term.id}>
<dt>{term.name}</dt>
<dd>{term.definition}</dd>
</Fragment>
));Now the fragment is the element directly returned by map(), it has the key, and the dt plus dd stay associated with the same term.
Nothing special happening beyond that.
You can't read key from props
This one is a bit weird the first time you see it.
You write:
<LessonItem
key={lesson.id}
lesson={lesson}
/>Then maybe inside the component you try:
function LessonItem({ key, lesson }) {
return <li>{key} {lesson.title}</li>;
}But key isn't available there as a normal prop.
React reads key itself because React needs it for matching elements. It doesn't pass that value through to your component as part of props.
So if your component also needs the lesson ID, pass it separately:
<LessonItem
key={lesson.id}
lessonId={lesson.id}
lesson={lesson}
/>Then:
function LessonItem({ lessonId, lesson }) {
return (
<li data-lesson-id={lessonId}>
{lesson.title}
</li>
);
}Same value, two uses.
React uses key.
Your component uses lessonId.
A React key is also not a DOM id
Another thing that can be confusing when you're new to this.
If you write:
<LessonItem key={lesson.id} lesson={lesson} />React does not put that key into the HTML.
You won't open DevTools and suddenly see:
<li key="lesson-123">That doesn't happen.
key belongs to React's own element matching.
If you actually need an HTML ID, write one:
<li id={`lesson-${lesson.id}`}>
{lesson.title}
</li>Or maybe a data attribute:
<li data-lesson-id={lesson.id}>
{lesson.title}
</li>Those go into the DOM because you explicitly asked for them.
A React key and an HTML id are doing different jobs. HTML IDs also have document-level uniqueness rules, while React keys only need to be unique among the siblings React is comparing.
Sometimes one field isn't enough for an ID
Maybe your data doesn't have one nice id field and a record is identified by two values together.
You can build a key from both:
const key = `${readerId}:${lessonId}`;And use it:
<LessonItem key={`${readerId}:${lessonId}`} />That's okay if those values stay the same for that record and can't accidentally produce duplicate strings.
Though if your backend already gives this record a proper ID, I'd rather use that. Less identity logic sitting in the UI, less stuff to remember later.
What you definitely don't want is temporary UI state inside the key:
key={`${lesson.id}:${lesson.complete}`}Imagine lesson.complete changes from false to true.
Now the key changes too.
React sees a different key and can create a fresh component instance.
But we didn't get a different lesson. We just marked the same lesson complete.
So the key should still be lesson.id.
Changing a key can intentionally reset a component
Okay, so far I've mostly been telling you not to change keys randomly.
But changing one can also be exactly what you want.
Suppose we have an editor:
<LessonEditor
key={lesson.id}
lesson={lesson}
/>You're editing lesson A, and LessonEditor has some local state.
Then you select lesson B.
lesson.id changes, which means the key changes too.
React now treats this as another component instance, so local state from lesson A doesn't carry over into lesson B's editor.
In this case, good. We're editing a different record.
So don't come away with "keys should never change". They should change when the identity represented by that element actually changes.
If the same lesson moves from index 2 to index 7, keep the same key.
If the component used to represent lesson A and now represents lesson B, changing the key can be exactly right.
What you shouldn't do is change keys just because the UI didn't update and you want to force React to recreate everything.
<Component key={Date.now()} />Please don't.
If your component only updates after doing that, there's probably some state or prop problem that needs fixing.
You can actually watch index keys go wrong
Let's make the problem visible instead of only talking about it.
Put an uncontrolled input inside every item:
function LessonItem({ lesson }) {
return (
<li>
{lesson.title}
<input defaultValue={lesson.note} />
</li>
);
}Now render the list with index keys:
lessons.map((lesson, index) => (
<LessonItem
key={index}
lesson={lesson}
/>
));Type something into the first input.
Don't save that value anywhere. Just type into it so the DOM input itself currently holds the value.
Now insert a new lesson at the beginning of the array.
You can end up seeing that typed input value beside the newly inserted lesson.
Why?
Because key 0 still exists.
React matched the old element with key 0 to the new element with key 0, even though the data record at index zero is now a different lesson.
Change it to:
lessons.map((lesson) => (
<LessonItem
key={lesson.id}
lesson={lesson}
/>
));Try again.
Now the original lesson keeps its own identity while moving down the list, and the new lesson gets a new item instance.
This little input test is actually a pretty good way to understand keys because you can see the wrong matching happen instead of only reading about reconciliation and hoping it made sense.
So what should your key look like?
For most normal application lists, you're looking for something pretty simple.
The key should come from the record itself, stay the same across renders for that same record, be unique among the siblings in that list, and sit on the element your map() callback directly returns.
So this:
lessons.map((lesson) => (
<LessonItem
key={lesson.id}
lesson={lesson}
/>
));is usually exactly what you want.
key={index} can break when positions change. key={Math.random()} or crypto.randomUUID() during render changes every time. Editable titles can change. UI state shouldn't be part of identity.
Use the actual record ID when you have one.
And once you understand why React needs that ID, the annoying "Each child in a list should have a unique key prop" warning makes a lot more sense.
React isn't asking for some random unique string just to make the warning go away.
It's asking: "When I see these siblings again on the next render, how do I know which one is which?"