React 23 ⚛️ Rendering Lists with Array Map
A list is the most common shape of data in any interface. A table of users, a feed of posts, a menu of links, a set of options. React does not provide a v-for directive or an ngFor attribute. It does not need one. JSX is JavaScript, and JavaScript has Array.prototype.map, which transforms an array of data into an array of elements. This is not a workaround or a React-specific pattern; it is the idiomatic way to render lists in React, and it follows directly from the design of JSX as an expression language.
This chapter covers list rendering with map in React. You will learn how map transforms data into JSX, why the key prop is required, what happens when keys are missing or wrong, and how keys differ from the key attribute in other frameworks. You will see how to render lists of components, how to filter and sort before mapping, and how to avoid the performance traps that come from inline functions and unstable keys.
By the end, you will understand not just the syntax but the reconciliation algorithm underneath it—how React uses keys to match elements between renders, why index keys break on reorder, and how to choose a key that keeps your list stable and correct.
Key point: The key prop is not passed to your component. It is a hint to React’s reconciliation algorithm. React uses it to identify which elements in a list are the same between renders, so it can preserve component state and DOM nodes for items that have not changed.
Why map and keys exist in React
The repetition problem. Rendering a list of items manually means duplicating JSX for each item, which is impossible when the list is dynamic. JavaScript’s map method transforms an array into a new array by applying a function to each element. In React, that function returns JSX, and the result is an array of elements that React renders in order. There is no special syntax because none is needed—map already does exactly what list rendering requires.
The identity problem. When React re-renders a list, it compares the new array of elements against the previous one to decide what to update. Without a way to identify each element, React can only compare by position. If the list order changes, React may reuse DOM nodes for the wrong items, causing state to attach to the wrong elements. The key prop solves this by giving each element a stable identity. React uses the key to match elements between renders, regardless of their position.
The index key trap. A common mistake is to use the array index as the key. This seems to work because the index is always present and always unique within a single render. But the index is not stable across renders. If items are added, removed, or reordered, the index for a given item changes. React then treats the element at that index as a different element, discarding state and recreating DOM nodes. This causes subtle bugs with form inputs, animations, and component state.
The performance problem. Keys enable React to perform minimal DOM updates. Without keys, React falls back to a slower reconciliation path. With stable keys, React can move existing DOM nodes rather than recreating them, which is significantly faster for large or frequently updated lists.
The React philosophy. React does not invent a list directive because JavaScript already has one. The map method is a general-purpose array transformation, and rendering a list is just a specific case of transforming data into UI. This means React developers use the same map, filter, and sort methods they already know, applied to JSX instead of other values.
a. Basic list rendering with map
The map method takes a callback that receives each item and returns a value. In JSX, that value is an element.
function FruitList() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
The callback returns a <li> element for each fruit. The result is an array of elements, which React renders in order. The key prop is required on the outermost element returned by the callback. Without it, React logs a warning in development mode.
The key should be unique among siblings, not globally. Two different lists can use the same keys without conflict. What matters is that within a single list, each key identifies a distinct item.
For arrays of objects, the key is usually a unique ID from the data:
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
The key is not passed to the <li> as a prop. It is consumed by React during reconciliation. If you need the ID inside the component, pass it as a separate prop.
b. The key prop and reconciliation
React’s reconciliation algorithm compares the previous tree of elements with the new one and computes the minimum set of changes needed. For lists, it uses the key prop to match elements between renders.
When keys are stable, React can:
- Preserve the DOM node for an item that has not changed.
- Move DOM nodes when the order changes, rather than recreating them.
- Preserve component state for items that remain in the list.
When keys are missing or unstable, React falls back to comparing elements by position. This means:
- If items are reordered, React updates the content of each position rather than moving the elements.
- Component state stays with the position, not the item, causing inputs and other stateful elements to show stale values.
- DOM nodes are destroyed and recreated more often than necessary, which is slower.
The difference is easiest to see with inputs. Consider a list where each item has a text input:
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input defaultValue={todo.text} />
</li>
))}
</ul>
);
}
If you type into the inputs and then reorder the todos array, the input values stay in their positions while the labels move. This is the index key trap in action. The fix is to use a stable ID:
{todos.map((todo) => (
<li key={todo.id}>
<input defaultValue={todo.text} />
</li>
))}
Now the inputs move with their items, and the values stay correct.
The key must be a string or number. React converts it to a string internally. Using an object or array as a key causes a warning and may not work as expected.
c. Filtering, sorting, and composing with map
Real lists rarely render the raw array. They filter, sort, or transform it first. These operations compose naturally because they are all array methods.
function ActiveUserList({ users }) {
return (
<ul>
{users
.filter((user) => user.active)
.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
The filter runs first and produces a smaller array. The map then transforms each remaining user into an element. The key still comes from the user’s ID, not from the position in the filtered array.
Sorting works the same way:
{users
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => (
<li key={user.id}>{user.name}</li>
))}
The .slice() call is important because sort mutates the array in place. Without it, you would be sorting the original array, which can cause bugs in other parts of the component. Copying first keeps the operation pure.
When the list is rendered inside a component that receives the array as a prop, the operations can be extracted to a variable for readability:
function UserList({ users }) {
const activeUsers = users
.filter((u) => u.active)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{activeUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This also allows the computation to be wrapped in useMemo if it is expensive, though for most lists the cost is negligible.
For lists of components rather than elements, the same pattern applies:
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
The key goes on the component, not inside it. The component receives todo as a prop but not key.
Complete Example Session
// ============================================
// PART 1: BASIC STRING LIST
// ============================================
function FruitList() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
// ============================================
// PART 2: LIST OF OBJECTS
// ============================================
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
);
}
// ============================================
// PART 3: LIST OF COMPONENTS
// ============================================
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// ============================================
// PART 4: THE INDEX KEY TRAP
// ============================================
function BadTodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input defaultValue={todo.text} />
</li>
))}
</ul>
);
}
// Reordering todos leaves input values in place
// ============================================
// PART 5: STABLE KEY FIX
// ============================================
function GoodTodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input defaultValue={todo.text} />
</li>
))}
</ul>
);
}
// Inputs move with their items
// ============================================
// PART 6: FILTERING BEFORE MAP
// ============================================
function ActiveUsers({ users }) {
return (
<ul>
{users
.filter((user) => user.active)
.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ============================================
// PART 7: SORTING BEFORE MAP
// ============================================
function SortedUsers({ users }) {
const sorted = [...users].sort((a, b) =>
a.name.localeCompare(b.name)
);
return (
<ul>
{sorted.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ============================================
// PART 8: COMPOSED FILTER, SORT, MAP
// ============================================
function ActiveSortedUsers({ users }) {
const visible = users
.filter((u) => u.active)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{visible.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ============================================
// PART 9: NESTED LISTS
// ============================================
function CategoryList({ categories }) {
return (
<div>
{categories.map((category) => (
<div key={category.id}>
<h3>{category.name}</h3>
<ul>
{category.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
// ============================================
// PART 10: EMPTY LIST HANDLING
// ============================================
function ItemList({ items }) {
if (items.length === 0) {
return <p>No items found.</p>;
}
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
The ten parts covered the essential patterns: basic string list, list of objects, list of components, the index key trap, the stable key fix, filtering, sorting, composed filter-sort-map, nested lists, and empty list handling.
Quick Reference
map Syntax
| Form | Example |
|---|---|
| Basic | {items.map((item) => <li key={item.id}>{item}</li>)} |
| With index | {items.map((item, i) => <li key={item.id}>{i}</li>)} |
| Components | {items.map((item) => <Item key={item.id} data={item} />)} |
| Multi-line | {items.map((item) => (\n <li key={item.id}>\n {item.name}\n </li>\n))} |
Key Rules
| Rule | Requirement |
|---|---|
| Unique among siblings | Yes |
| Stable across renders | Yes |
| String or number | Yes (objects not allowed) |
| Not a prop | React consumes it |
| Required for lists | Yes (warning without) |
Key Selection
| Key Choice | When Appropriate |
|---|---|
| Database ID | Always, when available |
| Composite key | When no single unique field |
| Index | Static lists only |
| Random value | Never (breaks reconciliation) |
| Content string | When content is unique and stable |
Array Methods with map
| Method | Purpose | Order |
|---|---|---|
filter | Remove items | Before map |
sort | Order items | Before map (copy first) |
slice | Limit items | Before map |
map | Transform to JSX | Last |
reverse | Reverse order | Before map (copy first) |
Best Practices
✅ Do This:
// Use unique, stable IDs as keys
{users.map((user) => <li key={user.id}>{user.name}</li>)} // ✅
// Put key on the outermost element
{todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} // ✅
// Copy before sorting
{[...users].sort(compare).map((u) => <li key={u.id} />)} // ✅
// Filter before map
{users.filter((u) => u.active).map((u) => <li key={u.id} />)} // ✅
// Handle empty lists explicitly
if (items.length === 0) return <Empty />; // ✅
// Extract complex computations
const visible = users.filter(...).sort(...); // ✅
❌ Don’t Do This:
// Use index as key for dynamic lists
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)} // ❌
// Omit key entirely
{todos.map((todo) => <li>{todo.text}</li>)} // ❌
// Use random values as keys
{todos.map((todo) => <li key={Math.random()}>{todo.text}</li>)} // ❌
// Mutate the source array
{users.sort(compare).map((u) => <li key={u.id} />)} // ❌
// Use non-primitive keys
{todos.map((todo) => <li key={todo}>{todo.text}</li>)} // ❌
// Return multiple elements without a fragment
{todos.map((todo) => (
<dt key={todo.id}>{todo.term}</dt>
<dd>{todo.definition}</dd>
))} // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Input values stay in place | Index used as key | Use stable ID |
| Missing key warning | Key prop omitted | Add key={item.id} |
| Random keys break state | New key each render | Use stable identifier |
| Source array mutated | sort or reverse called directly | Copy with [...arr] first |
| Nothing renders | map returns empty array | Check filter conditions |
| Key not accessible in component | Tried to read props.key | Pass as separate prop |
| Multiple elements without key | Fragment needed | Wrap in <Fragment key={...}> |
Real-World Examples
1. Navigation Menu
{links.map((link) => (
<li key={link.href}>
<a href={link.href}>{link.label}</a>
</li>
))}
2. Table Rows
{users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
3. Select Options
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
4. Comment Thread
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
5. Filtered Search Results
{results
.filter((r) => r.title.includes(query))
.map((r) => <ResultItem key={r.id} result={r} />)}
6. Sorted Product Grid
{[...products]
.sort((a, b) => a.price - b.price)
.map((p) => <ProductCard key={p.id} product={p} />)}
7. Tag List
{tags.map((tag) => (
<span key={tag} className="tag">{tag}</span>
))}
8. Nested Menu
{categories.map((cat) => (
<div key={cat.id}>
<h2>{cat.name}</h2>
<ul>
{cat.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
9. Form Field List
{fields.map((field) => (
<div key={field.id}>
<label>{field.label}</label>
<input name={field.name} />
</div>
))}
10. Empty State Fallback
{items.length === 0 ? (
<EmptyState />
) : (
items.map((item) => <Item key={item.id} data={item} />)
)}
Visual
Map Transformation
┌─────────────────────────────────────────────────────────────┐
│ MAP TRANSFORMS DATA TO ELEMENTS │
│ │
│ Input array: │
│ [{id: 1, name: 'Alice'}, │
│ {id: 2, name: 'Bob'}, │
│ {id: 3, name: 'Carol'}] │
│ │
│ .map(user => <li key={user.id}>{user.name}</li>) │
│ │
│ Output array: │
│ [<li key="1">Alice</li>, │
│ <li key="2">Bob</li>, │
│ <li key="3">Carol</li>] │
│ │
│ React renders each element in order. │
│ │
└─────────────────────────────────────────────────────────────┘
Reconciliation with Keys
┌─────────────────────────────────────────────────────────────┐
│ STABLE KEYS: ELEMENTS MOVE WITH IDENTITY │
│ │
│ Before: After (reordered): │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ key: A │ │ key: C │ ← moved from pos 3 │
│ │ key: B │ │ key: A │ ← moved from pos 1 │
│ │ key: C │ │ key: B │ ← moved from pos 2 │
│ └─────────────┘ └─────────────┘ │
│ │
│ React moves DOM nodes. State stays with each item. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ INDEX KEYS: ELEMENTS STAY IN POSITION │
│ │
│ Before: After (reordered): │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ key: 0 (A) │ │ key: 0 (C) │ ← content patched │
│ │ key: 1 (B) │ │ key: 1 (A) │ ← content patched │
│ │ key: 2 (C) │ │ key: 2 (B) │ ← content patched │
│ └─────────────┘ └─────────────┘ │
│ │
│ React reuses nodes by position. State stays in place. │
│ Input values mismatch their labels. │
│ │
└─────────────────────────────────────────────────────────────┘
The Index Key Trap
┌─────────────────────────────────────────────────────────────┐
│ WHY INDEX KEYS BREAK STATE │
│ │
│ Initial: [{id: 1, text: 'Buy milk'}, │
│ {id: 2, text: 'Walk dog'}] │
│ Keys: [0, 1] │
│ │
│ User types "X" into the second input. │
│ Input at index 1 contains "X". │
│ │
│ Reorder: [{id: 2, text: 'Walk dog'}, │
│ {id: 1, text: 'Buy milk'}] │
│ Keys: [0, 1] ← unchanged! │
│ │
│ React sees same keys, patches content in place: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ index 0: "Walk dog" input has old value │ │
│ │ index 1: "Buy milk" input has "X" ← WRONG │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ With ID keys, inputs move with their items. │
│ │
└─────────────────────────────────────────────────────────────┘
Filter, Sort, Map Pipeline
┌─────────────────────────────────────────────────────────────┐
│ ARRAY METHOD COMPOSITION │
│ │
│ users │
│ │ │
│ ▼ │
│ .filter(u => u.active) ──▶ [active users] │
│ │ │
│ ▼ │
│ .sort((a, b) => ...) ──▶ [sorted active users] │
│ │ │
│ ▼ │
│ .map(u => <li key={u.id}>{u.name}</li>) │
│ │ │
│ ▼ │
│ [<li>, <li>, <li>] ──▶ React renders │
│ │
│ Each step produces a new array. No mutation. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| List rendering | array.map(item => <Element />) |
| Key prop | Required on outermost element |
| Key type | String or number |
| Key uniqueness | Among siblings |
| Stable key | Preserves state on reorder |
| Index key | Breaks on reorder |
| Filter | Before map |
| Sort | Before map, copy first |
| Empty list | Handle with early return |
| Nested lists | Keys at each level |
Key takeaways:
mapis the list rendering method. React does not need a directive because JavaScript’smapalready transforms arrays into arrays of elements. The pattern is idiomatic, not a workaround.- The
keyprop is required. Without it, React logs a warning and falls back to positional reconciliation, which causes bugs when the list changes order. - Keys must be stable and unique among siblings. A database ID is ideal. A composite key works when no single field is unique. The array index is the worst choice for dynamic lists.
- The index key trap is real. Using the index as a key means React matches elements by position, not identity. When the list reorders, state stays with the position rather than the item, causing input values to mismatch their labels.
keyis not passed to the component. React consumes it during reconciliation. If a component needs the ID, pass it as a separate prop.- Filter and sort before mapping. These operations compose naturally because they all return arrays. Copy the array before sorting to avoid mutating the source.
- Handle empty lists explicitly. An empty array produces an empty list, which renders nothing. A conditional check for the empty state provides a better user experience.
- Nested lists need keys at each level. Each
mapcall requires a key on its outermost element, including nested lists inside parent lists.
Remember: List rendering in React is array transformation. The map method is the tool, the key prop is the identity hint, and the reconciliation algorithm is what makes it all work. When keys are stable, React can move elements efficiently and preserve their state. When keys are unstable, React falls back to positional matching, and bugs follow. Choose a key that identifies the item, not its position. Filter and sort before mapping, handle the empty case, and keep the render function readable. The list will render correctly, update efficiently, and behave predictably across every change.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!