ποΈ Rendering Lists with map
Almost every real interface is a list: a feed of posts, a table of orders, a menu of options, a cart of products. React has no special "list" syntax β instead you take a plain JavaScript array and transform it into an array of elements with Array.prototype.map(). Learn that one pattern well and half of UI building falls into place.
Week 4 · Day 4 (Thursday: Lists and Conditional Rendering) · Lecture 1
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain why React renders collections with
map()instead of a loop - Render a list of primitives and a list of objects into JSX
- Extract a list item into its own reusable component and pass props to it
- Combine
filter(),sort(), andmap()to derive a view from state - Attach a stable
keyto every rendered item and say why it matters - Handle the empty, loading, and error states a real list must cover
Estimated Time: 60 minutes
Practice: Build a filterable, sortable product catalog that renders from an array of data.
In This Lesson
Why map?
In plain JavaScript you might build a list of DOM nodes with a for loop, pushing elements into the page one at a time. React works differently: a component is a function that returns a description of UI. To describe ten list items you need ten elements sitting inside your JSX β and the cleanest way to produce ten elements from ten pieces of data is to transform the data array into an element array.
That transformation is exactly what Array.prototype.map() does. It walks an array, runs a function on each entry, and returns a new array of the results. Feed it data, return JSX, and you get an array of elements React can render.
[a, b, c]"] --> B["map(item => <li>)"] B --> C["Array of elements
[<li>, <li>, <li>]"] C --> D["React renders
the elements"]
Think of map() as a stamping machine on an assembly line. The raw material (each data item) goes in, the same template is applied to every one, and finished parts (elements) come out in the same order. Because it returns a brand-new array and never mutates the original, it fits React's core rule perfectly: derive UI from data, don't mutate the data to change the UI.
π map vs forEach
forEach() runs a function for its side effects and returns undefined β useless in JSX, because JSX needs a value. map() collects and returns the results. In React you almost always want map(). If you catch yourself reaching for forEach inside JSX, you probably want map.
Rendering a Basic List
Start with the simplest case: an array of strings. Wrap the map() call in curly braces so JSX evaluates it as an expression, and return one element per item.
function FruitList() {
const fruits = ['Apple', 'Banana', 'Orange', 'Mango', 'Pineapple'];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
// What happens, step by step:
// 1. fruits is a plain array of 5 strings.
// 2. map() turns each string into an <li> element.
// 3. The result is an array of 5 <li> elements.
// 4. React renders that array inside the <ul>.
Notice the key={fruit}. React asks for a unique key on each item in a rendered list so it can track items across re-renders. We use the fruit name here because the names happen to be unique; the very next lesson is devoted to choosing keys well. For now, the rule is simply: every item you map must get a key.
β οΈ Curly braces, not a statement
JSX only accepts expressions inside { }, not statements. That is why you cannot drop a for loop or an if block directly into markup β but array.map(...) is an expression that evaluates to an array, so it slots right in.
Rendering into a fragment
You do not need a wrapping <ul>. When you just need siblings without an extra DOM node, map into a <>...</> fragment. The key then goes on whatever element you return:
function Tags({ tags }) {
return (
<>
{tags.map((tag) => (
<span key={tag} className="tag">#{tag}</span>
))}
</>
);
}
Lists of Objects
Real data is rarely a bare string. Usually each item is an object with several fields β and, crucially, its own stable identifier. That id is the natural key.
function ProductList() {
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 599 },
{ id: 3, name: 'Tablet', price: 399 },
];
return (
<div className="product-list">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>Price: ${product.price}</p>
<button>Add to Cart</button>
</div>
))}
</div>
);
}
Here the key is product.id β a value that belongs to the data and never changes for that product. That is exactly what a key should be. Compare it to the index of the item in the array, which changes the moment you sort or remove something. (Again: full treatment next lesson.)
Rendered result
Laptop β Price: $999 [Add to Cart]
Phone β Price: $599 [Add to Cart]
Tablet β Price: $399 [Add to Cart]
Extracting a List Item Component
When a list item grows beyond a line or two of JSX, pull it out into its own component. The map() then becomes a clean one-liner that passes each object's fields down as props. This keeps the list readable and makes the item reusable and independently testable.
// A focused component for one product.
function Product({ name, price, inStock }) {
return (
<div className="product">
<h3>{name}</h3>
<p>Price: ${price}</p>
<p className={inStock ? 'in-stock' : 'out-of-stock'}>
{inStock ? 'In Stock' : 'Out of Stock'}
</p>
</div>
);
}
// The list just maps data onto that component.
function ProductGrid() {
const products = [
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Phone', price: 599, inStock: false },
{ id: 3, name: 'Tablet', price: 399, inStock: true },
];
return (
<div className="product-grid">
{products.map((product) => (
<Product
key={product.id}
name={product.name}
price={product.price}
inStock={product.inStock}
/>
))}
</div>
);
}
π‘ Key goes on the outermost element
The key belongs on the element you return from map() β here that is <Product>, not the <div> inside Product. React reads the key from the array item, so it must live at the top level of the mapped output. You also cannot read the key back as a prop inside Product; it is reserved for React. If the child needs the id, pass it again explicitly (e.g. id={product.id}).
Spreading props
If the object's field names already match the component's props, the spread operator saves repetition β but pass key separately, since it is not a real prop:
{products.map((product) => (
<Product key={product.id} {...product} />
))}
Filtering & Sorting Before Mapping
You almost never render all the raw data exactly as stored. You render a view of it: only the items that match a filter, in a chosen order. Because filter(), sort(), and map() all work on arrays, you can chain them. The golden rule: derive the view during render from state β never mutate the source array.
Filter, then map
import { useState } from 'react';
function FilterableProducts() {
const products = [
{ id: 1, name: 'Laptop', price: 999, category: 'electronics' },
{ id: 2, name: 'Shirt', price: 29, category: 'clothing' },
{ id: 3, name: 'Phone', price: 599, category: 'electronics' },
{ id: 4, name: 'Shoes', price: 89, category: 'clothing' },
];
const [filter, setFilter] = useState('all');
// Derived data: computed fresh on every render from state + props.
const visible = products.filter(
(p) => filter === 'all' || p.category === filter
);
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<ul>
{visible.map((p) => (
<li key={p.id}>{p.name} β ${p.price}</li>
))}
</ul>
</div>
);
}
Sort without mutating
Array.prototype.sort() sorts in place and returns the same array. Sorting your state array directly would mutate state β a bug. Always copy first with the spread operator, then sort the copy:
function SortableUsers() {
const [users] = useState([
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 25 },
{ id: 3, name: 'Charlie', age: 35 },
]);
const [sortBy, setSortBy] = useState('name');
// [...users] makes a shallow copy so we never mutate state.
const sorted = [...users].sort((a, b) =>
sortBy === 'name'
? a.name.localeCompare(b.name)
: a.age - b.age
);
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="age">Sort by Age</option>
</select>
<ul>
{sorted.map((u) => (
<li key={u.id}>{u.name} β Age: {u.age}</li>
))}
</ul>
</div>
);
}
β Notice the keys survive sorting
Because the key is u.id β tied to the data, not the position β React correctly follows each row as it moves to a new spot when the sort order changes. If we had keyed by array index, React would think the content changed rather than the order. That distinction is the heart of the next lesson.
Keys: A First Look
Every time you map data to elements, React wants a key β a string or number that uniquely identifies each item among its siblings. Keys are React's way of matching each element in the new render to the corresponding element in the previous render, so it can update, insert, or remove the minimum number of DOM nodes.
The quick rules you will apply today:
- Use a stable, unique id from your data (
item.id) whenever one exists. - If there is no id, derive one from unique content (
key={fruit}). - Reach for the array index only for a static list that never reorders, filters, or gets items inserted.
β οΈ Why index-as-key bites you (preview)
Keys tell React which old element maps to which new one. If you key by index and then insert an item at the front, every item's index shifts β so React thinks each row's content changed and can leave stale state (like text typed into an input) attached to the wrong row. The next lesson walks through a concrete reorder that corrupts input values. For now: prefer real ids.
Empty, Loading & Error States
A list of zero items is not an error β but rendering an empty <ul></ul> leaves the user staring at nothing. Production lists explicitly handle three non-happy states before the data ever arrives: loading, error, and empty. The early-return pattern keeps each case clean.
import { useState, useEffect } from 'react';
function ContactList() {
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchContacts()
.then((data) => setContacts(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading contactsβ¦</p>;
if (error) return <p role="alert">Couldnβt load contacts: {error}</p>;
// The "empty" branch: valid data, just none of it.
if (contacts.length === 0) {
return (
<div className="empty-state">
<p>No contacts yet.</p>
<button onClick={() => window.location.reload()}>Refresh</button>
</div>
);
}
// The happy path: we know there is at least one item.
return (
<ul>
{contacts.map((c) => (
<li key={c.id}>{c.name}</li>
))}
</ul>
);
}
Reading top to bottom, each guard peels off one case, and by the time you reach the map() you are guaranteed real, non-empty data. This is the same early-return idea you will use throughout the conditional-rendering lesson two lessons from now.
π‘ The empty check goes before map, not inside it
You cannot "map your way" to an empty state β [].map(...) just produces nothing. Decide what to show for zero items before you map, with a length check. Handle it once, at the top, and the list body stays simple.
Practice & Quiz
ποΈ Exercise 1: Render a leaderboard
Goal: Given an array of players, render an ordered list showing each player's name and score, sorted highest score first β without mutating the original array.
const players = [
{ id: 'p1', name: 'Ada', score: 42 },
{ id: 'p2', name: 'Grace', score: 88 },
{ id: 'p3', name: 'Alan', score: 71 },
];
function Leaderboard() {
// TODO: render an <ol> of "Name β Score", highest score first.
// Do not mutate `players`. Give each <li> a stable key.
}
π‘ Hint
Copy first with [...players], then .sort((a, b) => b.score - a.score) for descending order, then .map() into <li key={p.id}>. Descending means b - a, not a - b.
β Solution
function Leaderboard() {
const ranked = [...players].sort((a, b) => b.score - a.score);
return (
<ol>
{ranked.map((p) => (
<li key={p.id}>{p.name} β {p.score}</li>
))}
</ol>
);
}
ποΈ Exercise 2: Filter with an empty state
Goal: Render only the in-stock products. If none are in stock, show "Everything is sold out" instead of an empty list.
β Solution
function InStockList({ products }) {
const available = products.filter((p) => p.inStock);
if (available.length === 0) {
return <p>Everything is sold out</p>;
}
return (
<ul>
{available.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
π― Quick Quiz
Question 1: Why do we use map() rather than forEach() to render a list in JSX?
Question 2: Where should the key prop go when you map onto a custom <Product> component?
Question 3: Why copy the array with [...users] before calling .sort()?
Best Practices & Pitfalls
β Do
- Give every mapped element a stable, unique
key - Derive filtered/sorted views during render; treat state and props as read-only
- Copy an array (
[...arr]) beforesort()orreverse() - Extract a component when a list item grows past a few lines
- Handle the empty case explicitly with a length check
β Don't
- Mutate the source array with
push,splice, or in-placesortin render - Use the array index as a key for a list that can reorder or receive inserts
- Forget the key and ignore React's console warning
- Nest a
forloop in JSX β usemap(), which is an expression
β οΈ The falsy-length trap
// β When items.length is 0, this renders a literal "0"!
{items.length && <List items={items} />}
// β
Compare explicitly so the left side is a real boolean
{items.length > 0 && <List items={items} />}
The number 0 is falsy, so && returns 0 β and React renders that zero on screen. We dig into this and other conditional-rendering gotchas in the "Conditional rendering patterns" lesson.
Summary
π Key Takeaways
- React renders collections by transforming a data array into an element array with
map() map()returns a new array and never mutates the source β perfect for React's data-driven model- Every mapped element needs a stable, unique
key; the key sits on the top-level returned element - Chain
filter()βsort()βmap()to derive a view, copying before any in-place sort - Handle loading, error, and empty states with early returns before the map
π Additional Resources
- react.dev β Rendering Lists
- react.dev β Passing arrays of JSX as children
- MDN β
Array.prototype.map()
π What's Next?
You have been sprinkling key props onto every item and taking on faith that they matter. The next lesson, Keys in React, opens the hood: how React's reconciliation uses keys, exactly what goes wrong when you key by index, and the concrete reorder/insert bug that corrupts component state.
π Great work!
You can now turn any array of data into a living, filterable, sortable list. That single skill powers feeds, tables, menus, carts β most of the UI you will ever build.