Keys: Identifying List Items
When React renders a list, it needs a way to track which items changed, were added, or were removed between renders. Keys are React's solution: a special string or number prop that gives React a stable identity for each element in a list. Understanding keys is essential for writing correct, bug-free list rendering.
Why Keys Exist: Reconciliation
React's reconciliation algorithm compares the previous render's virtual DOM tree with the new one to determine the minimal set of DOM changes needed. For lists, this comparison is tricky: if you insert an item at the beginning, did the existing items shift down, or were they replaced?
Without keys, React compares list items by their position — it assumes the first item is the same as the previous first item, the second is the same as the previous second, and so on. This is fast but wrong when the order changes.
With keys, React uses the key to match items across renders: if an item with key="abc" existed before and exists after, React knows it's the same item (even if its position changed) and reuses its DOM node. If an item with a previously unseen key appears, React creates a new DOM node.
The Warning Without Keys
// ✗ Missing key — React will log a warning:
// "Each child in a list should have a unique 'key' prop."
function List({ items }) {
return (
<ul>
{items.map(item => (
<li>{item.name}</li>
))}
</ul>
)
}
// ✓ With key — no warning, correct reconciliation
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}Using Data IDs as Keys
The best keys come from your data: database IDs, UUIDs, slugs — anything that uniquely identifies the item in your data model. These are stable (they don't change when the list is reordered) and globally unique within your dataset.
const contacts = [
{ id: 'user-1', name: 'Alice Chen', email: 'alice@example.com' },
{ id: 'user-2', name: 'Bob Okafor', email: 'bob@example.com' },
{ id: 'user-3', name: 'Carol Ray', email: 'carol@example.com' },
]
function ContactList() {
return (
<ul>
{contacts.map(contact => (
<li key={contact.id}>
<strong>{contact.name}</strong>
<span> — {contact.email}</span>
</li>
))}
</ul>
)
}
// After a sort or filter, React correctly matches each contact
// to its previous DOM node because the IDs are stableKeys Must Be Unique Among Siblings
Keys only need to be unique among siblings in the same list — not globally across your entire app. You can use the same key in different lists:
function TwoLists() {
const fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
]
const vegetables = [
{ id: 1, name: 'Carrot' }, // key=1 is fine here — different list
{ id: 2, name: 'Broccoli' },
]
return (
<>
<ul>
{fruits.map(f => <li key={f.id}>{f.name}</li>)}
</ul>
<ul>
{vegetables.map(v => <li key={v.id}>{v.name}</li>)}
</ul>
</>
)
}Index Keys: When They Work and When They Don't
React's documentation and lint rules warn against using the array index as a key. The reason: indices are unstable when the list changes order or items are inserted/removed.
// Index keys — appears to work but causes subtle bugs with reordering
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// Scenario: items = ['A', 'B', 'C']
// Keys: 0='A', 1='B', 2='C'
// User prepends 'Z':
// items = ['Z', 'A', 'B', 'C']
// Keys: 0='Z', 1='A', 2='B', 3='C'
// React sees:
// key=0: before='A', after='Z' → updates A's text to Z (DOM reuse!)
// key=1: before='B', after='A' → updates B's text to A
// key=2: before='C', after='B' → updates C's text to B
// key=3: new → creates C
// Instead of moving nodes, React overwrites content in place.
// This is inefficient AND breaks things like input state.Seeing the Bug: Index Keys with Inputs
// This component demonstrates the index key bug
function BadList() {
const [items, setItems] = useState(['A', 'B', 'C'])
function prepend() {
setItems(prev => ['Z', ...prev])
}
return (
<div>
<button onClick={prepend}>Prepend Z</button>
<ul>
{items.map((item, index) => (
<li key={index}>
{item} <input placeholder={`Note for ${item}`} />
</li>
))}
</ul>
</div>
)
}
// Step 1: Type "note A" in the first input
// Step 2: Click "Prepend Z"
// Bug: "note A" is now showing in the Z row — React reused the first input!
// Fix: use a stable key (data ID, or a stable identifier from the item itself)Keys Are Not Props
The key prop is special — React reads it internally and does not pass it to your component. If you need the ID inside your component, pass it as a separate prop:
function UserCard({ user }) {
// user.id is NOT accessible via props.key
console.log(user.id) // works — passed explicitly
return <div>{user.name}</div>
}
function UserList({ users }) {
return (
<ul>
{users.map(user => (
// key is consumed by React, id is passed to the component
<UserCard key={user.id} user={user} />
))}
</ul>
)
}Generating Stable Keys Without IDs
Sometimes your data doesn't have IDs — for example, a list of strings. A few strategies:
// 1. Use the value itself (works when values are unique)
const tags = ['react', 'typescript', 'nextjs']
{tags.map(tag => <Tag key={tag} label={tag} />)}
// 2. Combine multiple fields to form a unique key
const entries = [
{ date: '2024-01-01', category: 'food', amount: 12 },
{ date: '2024-01-01', category: 'travel', amount: 50 },
]
{entries.map(e => (
<EntryRow key={`${e.date}-${e.category}`} entry={e} />
))}
// 3. Add an ID when creating the data
function addItem(text) {
setItems(prev => [...prev, { id: crypto.randomUUID(), text }])
}
// Then: items.map(item => <li key={item.id}>{item.text}</li>)Keys let React track list items across re-renders for efficient reconciliation
Always use a unique, stable value from your data (database ID, UUID, slug)
Keys must be unique among siblings — not globally across the app
The
keyprop is not passed to your component — passidseparately if neededAvoid array index keys when the list can be reordered, filtered, or has internal state
Never use
Math.random()orDate.now()as keys — they change every render