Lists and Keys
Render dynamic collections efficiently by giving React a stable identity for each item.
Recipe
Quick-reference recipe card — copy-paste ready.
// Basic list rendering
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
// With Fragment keys (multiple elements per item)
import { Fragment } from "react";
{entries.map(entry => (
<Fragment key={entry.id}>
<dt>{entry.term}</dt>
<dd>{entry.definition}</dd>
</Fragment>
))}
// Filtering + mapping
{users
.filter(u => u.isActive)
.map(u => <UserCard key={u.id} user={u} />)}When to reach for this: Any time you render an array of data — todo items, search results, table rows, navigation links.
Working Example
"use client";
import { useState } from "react";
interface Todo {
id: string;
text: string;
done: boolean;
}
let nextId = 0;
function createId() {
return `todo-${++nextId}-${Date.now()}`;
}
export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [draft, setDraft] = useState("");
function addTodo() {
const text = draft.trim();
if (!text) return;
setTodos(prev => [...prev, { id: createId(), text, done: false }]);
setDraft("");
}
function toggleTodo(id: string) {
setTodos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
}
function removeTodo(id: string) {
setTodos(prev => prev.filter(t => t.id !== id));
}
return (
<div className="max-w-sm space-y-3 rounded border p-4">
<form
onSubmit={e => {
e.preventDefault();
addTodo();
}}
className="flex gap-2"
>
<input
value={draft}
onChange={e => setDraft(e.target.value)}
placeholder="Add a task..."
className="flex-1 rounded border px-3 py-1"
/>
<button type="submit" className="rounded bg-blue-600 px-3 py-1 text-white">
Add
</button>
</form>
{todos.length === 0 && (
<p className="text-sm text-gray-400">No tasks yet.</p>
)}
<ul className="space-y-1">
{todos.map(todo => (
<li key={todo.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span className={todo.done ? "flex-1 line-through text-gray-400" : "flex-1"}>
{todo.text}
</span>
<button
onClick={() => removeTodo(todo.id)}
className="text-xs text-red-500"
>
Remove
</button>
</li>
))}
</ul>
<p className="text-xs text-gray-500">
{todos.filter(t => !t.done).length} remaining
</p>
</div>
);
}What this demonstrates:
- Stable, unique
keybased on a generated ID, not array index - Immutable array updates:
mapto toggle,filterto remove, spread to add - Derived value (remaining count) computed inline from the array
- Empty state handled with a conditional
Deep Dive
How It Works
Array.prototype.map()inside JSX returns an array of elements — React renders each one- The
keyprop tells React which item is which across re-renders so it can match old and new elements - When an item's key stays the same, React updates the existing DOM node (preserving state like focus and input values)
- When a key is new, React mounts a new component. When a key disappears, React unmounts the old one
- Keys only need to be unique among siblings — not globally
Parameters & Return Values
| Prop | Type | Description |
|---|---|---|
key | string or number | Stable identity for each list item — must be unique among siblings |
What Makes a Good Key
| Source | Good? | Why |
|---|---|---|
| Database ID | Yes | Stable and unique by definition |
| UUID / nanoid | Yes | Unique, survives reordering |
item.slug | Yes | Stable if slugs don't change |
| Array index | Sometimes | Only safe for static lists that never reorder, filter, or insert |
Math.random() | No | Creates a new key every render — forces remount every time |
Variations
Nested lists:
{categories.map(cat => (
<section key={cat.id}>
<h2>{cat.name}</h2>
<ul>
{cat.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</section>
))}Sorted and filtered lists:
const visibleItems = useMemo(
() =>
items
.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name)),
[items, query]
);
return (
<ul>
{visibleItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);Resetting component state with key:
// Changing key forces React to unmount and remount the component
<PlayerProfile key={currentPlayerId} playerId={currentPlayerId} />TypeScript Notes
// Typed map callback
interface Product {
id: string;
name: string;
price: number;
}
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map(({ id, name, price }) => (
<li key={id}>
{name} — ${price.toFixed(2)}
</li>
))}
</ul>
);
}Gotchas
-
Index keys with reorderable lists — Using
key={index}in a sortable or filterable list causes React to reuse the wrong DOM nodes, leading to stale input values and broken animations. Fix: Use a stable unique ID from your data. -
Duplicate keys — Two siblings with the same key cause unpredictable behavior — React silently drops one. Fix: Ensure keys are unique. If your data has duplicates, combine fields:
key={\$-$`}`. -
Key on the wrong element — Placing
keyon the inner<span>instead of the outermost element returned bymapdoes nothing. Fix: Always putkeyon the element immediately returned by the.map()callback. -
Expensive list re-renders — A large list re-rendering on every parent render causes jank. Fix: Memoize list items with
React.memoand stabilize callbacks withuseCallback, or virtualize with@tanstack/react-virtual. -
Forgetting empty state — An empty array renders nothing, which can look like a broken UI. Fix: Always handle
items.length === 0with a placeholder message.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
@tanstack/react-virtual | Rendering thousands of items (virtualized list) | A short list (under 100 items) |
React.Children.map | Iterating over children prop elements | You have data arrays — use plain .map() |
CSS repeat() grid | Layout is purely visual repetition without dynamic data | Each item has unique data or state |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Three-level nested list rendering
// File: src/components/services/content-display.tsx
{content.sections.map((section) => {
const isLoaded = loadedSections.has(section.id);
const pointCount = section._count?.sectionPoints ?? section.sectionPoints?.length ?? 0;
return (
<AccordionItem key={section.id} value={`section-${section.id}`} className="border rounded-lg">
<AccordionContent className="px-6 pb-6">
{isLoaded && section.sectionPoints && section.topics && (
<>
{section.sectionPoints.map((point) => (
<PointCard
key={point.id}
point={point}
serviceSlug={content.slug}
sectionSlug={section.sectionId}
/>
))}
{section.topics.map((topic) => (
<div key={topic.id} className="mb-6 last:mb-0">
<h4 className="text-xl font-medium">{topic.topicTitle}</h4>
<div className="ml-6 space-y-3">
{topic.topicPoints.map((point) => (
<PointCard key={point.id} point={point} serviceSlug={content.slug} sectionSlug={section.sectionId} />
))}
</div>
</div>
))}
</>
)}
</AccordionContent>
</AccordionItem>
);
})}What this demonstrates in production:
- Three levels of nested
.map(): sections, topics, points, reflecting the data model hierarchy - Every mapped element uses
key={*.id}with stable database IDs (UUIDs), never array indices <>...</>(Fragment) wraps sibling lists without adding extra DOM nodes- Conditional
{isLoaded && ...}ensures points only render after lazy-loading completes for that section section._count?.sectionPoints ?? section.sectionPoints?.length ?? 0uses nullish coalescing to safely handle two different data shapes (Prisma _count vs populated arrays)- Deep nesting of
.map()calls can hurt readability. Consider extractingTopicListandPointListsub-components if it grows
FAQs
Why does React need a key prop on list items?
Keys tell React which item is which across re-renders. Without stable keys, React cannot correctly match old and new elements, leading to lost state, broken animations, and incorrect DOM reuse.
Is it ever okay to use array index as a key?
Only for static lists that never reorder, filter, or have items inserted/removed. For any dynamic list, use a stable unique ID from your data (database ID, UUID, or slug).
What happens if two siblings have the same key?
React silently drops one of them, causing unpredictable behavior. Always ensure keys are unique among siblings. Combine fields if needed: key={`${item.type}-${item.id}`}.
How do I render a list with filter and sort?
Chain .filter() and .sort() before .map(). Wrap in useMemo if the list is large:
const visible = useMemo(
() => items.filter(i => i.active).sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return <ul>{visible.map(i => <li key={i.id}>{i.name}</li>)}</ul>;How do I immutably add, remove, or update items in a list?
- Add:
setItems(prev => [...prev, newItem]) - Remove:
setItems(prev => prev.filter(i => i.id !== id)) - Update:
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i))
Where should the key prop go — on the outer or inner element?
Always on the outermost element returned by the .map() callback. Placing it on an inner child element has no effect.
Why is Math.random() a bad key?
It generates a new value every render, so React treats every item as new — unmounting and remounting all components each time. This destroys state and kills performance.
How do I handle an empty list?
Check items.length === 0 and render a placeholder message. An empty array renders nothing, which can look like a broken UI.
How do I use Fragment with a key in a list?
Import Fragment from React and use the named syntax:
import { Fragment } from "react";
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
))}The short syntax <> does not support keys.
How can I force a component to reset its state using key?
Change the key to a new value. React unmounts the old component and mounts a fresh one with initial state:
<PlayerProfile key={currentPlayerId} playerId={currentPlayerId} />When should I virtualize a list?
When rendering more than a few hundred items causes visible jank. Use @tanstack/react-virtual to only render items currently in the viewport, keeping DOM size small.
Related
- Conditional Rendering — filtering and empty states
- Components — extracting list items into their own components
- useState — managing list data as state