React Keys and Lists
Render dynamic collections with map and assign stable unique keys for predictable DOM reconciliation.
TL;DR
- Transform data arrays into component elements using standard
mapiterations. - Assign stable unique identifiers to the top-level
keyproperty. - Preserve component identity across DOM reorders by avoiding array
indexkeys.
List Rendering Basics
Array Map IterationMap over items returning an element for each data record.
export function TodoList({ todos }: TodoListProps) {
return (
<ListView>
{todos.map((todo) => (
<TodoRow key={todo.id} item={todo} />
))}
</ListView>
);
}Separate Component MapExtract list item markup into a dedicated subcomponent.
return (
<Grid>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</Grid>
);Empty List GuardProvide informative placeholder when collection is empty.
if (items.length === 0) {
return <EmptyNotice text="No records found." />;
}Key Selection Rules
Database ID KeyIdeal stable unique identifier derived from backend data.
items.map((item) => (
<ItemRow key={item.uuid} item={item} />
))Keyed FragmentAssign key to React.Fragment when mapping multiple tags.
import { Fragment } from 'react';
items.map((item) => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.def}</dd>
</Fragment>
))Composite KeyCombine stable unique fields when single IDs are absent.
items.map((it) => (
<Row key={`${it.category}-${it.code}`} />
))List Transformation Patterns
Filter and Map PipelineFilter active items before mapping to JSX elements.
const active = tasks
.filter((t) => !t.completed)
.map((t) => <Task key={t.id} task={t} />);Sorting CollectionsClone array before sorting to avoid direct prop mutation.
const sorted = [...items].sort((a, b) =>
a.name.localeCompare(b.name)
);Grouped ListsMap nested categories with independent keyed loops.
categories.map((cat) => (
<CategorySection key={cat.id}>
<CategoryTitle title={cat.name} />
{cat.items.map((it) => (
<ItemRow key={it.id} title={it.title} />
))}
</CategorySection>
))Reconciliation Pitfalls
Unstable Key BugRandom numbers force React to destroy and recreate DOM.
// BAD: Destroys DOM and state
items.map((item) => (
<Row key={Math.random()}>{item.text}</Row>
))Index Key DangerReordering causes inputs to retain wrong sibling state.
// DANGEROUS: shifts state on delete
items.map((item, idx) => (
<InputRow key={idx} />
))Duplicate Key ErrorDuplicate keys trigger console warnings and rendering bugs.
// Ensure IDs are distinct
const unique = Array.from(new Set(ids));Tips
- Always use natural database IDs or UUID strings as
keyvalues rather than relying on volatile loop indices. - Attach the
keyproperty directly to the outermost element returned inside themapcallback function.
Warnings
- Never use array indices as
keyprops when collections can be sorted, filtered, or rearranged dynamically. - Avoid generating random keys like
Math.random()on each render because this completely destroys internal DOM state.
In Practice
Render, filter, and sort tasks with guaranteed unique key reconciliation.
- Define task items with immutable unique identifier strings.
- Apply filtering logic based on selected completion status.
- Map filtered results to task rows using item ID keys.
- Provide fallback notification when filtered output is empty.
interface Task { id: string; title: string; }
export function TaskList({
tasks,
filterDone,
}: { tasks: Task[]; filterDone: boolean }) {
const list = tasks.filter((t) => filterDone || true);
if (list.length === 0) {
return <EmptyNotice text="No tasks found." />;
}
return (
<ListView>
{list.map((t) => (
<TaskRow key={t.id} task={t} />
))}
</ListView>
);
}FAQ
Keys give elements a stable identity between renders. React uses keys to match existing DOM nodes against newly rendered elements, preserving input focus and animations during reordering.
Array index is acceptable only if the list is strictly static, will never be sorted, filtered, or reordered, and list items have no internal state or form inputs.
The key prop must always be placed directly on the outermost JSX element or Fragment returned by the map function callback, not on nested children inside.