Rendering Lists
map() over data to produce elements, and handling the empty case.
What you will be able to do
- Turn an array of data into elements with map
- Render arrays of strings, of objects, and of components
- Put a stable key on the outermost element map returns
- Show an empty state instead of a blank area
- Filter before mapping, and render nested lists
- Give each item its own event handler without calling it during render
- Explain why forEach renders nothing
The idea, in plain English
Real data arrives as arrays - courses, modules, lessons, users. Rendering a list means turning each item in the array into an element, and React does not need a special loop for it: JavaScript already has map.
items.map((item) => <li>{item.title}</li>) returns a new array of elements, and React renders an array by rendering each element in turn. That is all list rendering is. Everything else in this lesson - keys, empty states, filtering, nesting - is about doing it well.
Two habits separate a demo from a real list. Every item gets a stable key, so React can tell items apart when the list changes. And every list has an empty state, because an empty array renders nothing at all, and a blank area looks like a bug.
Worked example: A course list that renders nothing gracefully.
map turns data into elements
map calls a function once per item and collects the return values into a new array. Return JSX from that function and you get an array of elements: ["React", "Node.js"].map((c) => <li>{c}</li>) is [<li>React</li>, <li>Node.js</li>].
Put that expression in braces inside a ul, and React renders each element as a child of the ul. The array itself never appears on the page - only what is in it.
Implicit and explicit returns
An arrow function with parentheses after the arrow returns what is inside them: (course) => (<li>{course.title}</li>). This is the form you will see most.
An arrow function with braces needs a return: (course) => { const title = course.title.toUpperCase(); return <li>{title}</li>; }. Use it when you need a line of logic before the JSX.
Mixing them up is a classic bug. (course) => { <li>{course.title}</li> } has braces but no return, so every call returns undefined and the list renders empty - with no error.
Keys
Every element returned from map needs a key prop: <li key={course.id}>. Without one, React logs "Each child in a list should have a unique key prop."
The key is the item identity. When the list changes, React matches old and new items by key, so it knows which ones were added, removed, or moved. Use an id from the data. Keys only need to be unique among siblings - the same ids can appear in a different list.
The key goes on the outermost element the callback returns. When that is a component, it goes on the component: <CourseCard key={course.id} course={course} />, not on an element inside CourseCard.
Lesson 10 is entirely about keys: what goes wrong with duplicate, index, and random keys, with a working demonstration.
key is not a prop
React takes key for itself; it never reaches the component. Inside CourseCard, props.key is undefined, and React 19 in development warns "key is not a prop. Trying to access it will result in undefined being returned."
If the component needs the id, pass it again under another name: <CourseCard key={course.id} courseId={course.id} course={course} /> - or just read course.id from the course you already passed.
Rendering components from data
In real code the callback returns a component, not raw markup: {courses.map((course) => <CourseCard key={course.id} course={course} />)}.
The split is worth keeping. The list component owns the array - fetching, filtering, sorting, the empty state. The item component owns how one item looks. Each can change without touching the other.
The empty state
An empty array maps to an empty array, and React renders an empty array as nothing. There is no error - just a blank space where the list should be, which users read as broken or still loading.
Check the length and show something useful. When the empty state replaces the whole component, an early return reads best: if (courses.length === 0) return <p>No courses available.</p>;. When it is one piece of a larger component, a ternary works: {courses.length > 0 ? <ul>...</ul> : <p>No courses yet.</p>}.
Whatever you use, compare the length explicitly. {courses.length && <ul>...</ul>} prints 0 for an empty list.
filter, then map
To show only some items, filter first and map the result: courses.filter((c) => c.published).map((c) => <CourseCard key={c.id} course={c} />).
When the filtered list can be empty, store it in a variable first - const published = courses.filter(...) - so you can check its length for the empty state before mapping it.
mapA new array, one result per item. The method for rendering.filterA new array with only the items that pass. Usually before map.forEachReturns undefined. Renders nothing - never use it in JSX.findOne item or undefined - for a detail view, not a list.some / everyA boolean - handy for conditions like "any lesson completed?".slice / toSortedCopies to limit or sort before rendering, without changing the original.Nested lists
Data is often nested: a course has modules, a module has lessons. Map the outer array, and inside each item map the inner one: modules.map((module) => <section key={module.id}>... {module.lessons.map((lesson) => <li key={lesson.id}>...)}</section>).
Each map needs its own keys, unique among its own siblings. Once nesting goes past two levels, pull the inner level into its own component - ModuleSection rendering LessonList - so each map stays short.
Events in lists
Each item can have its own handler: onClick={() => handleSelect(course.id)}. The arrow function captures that item id, and runs only when the button is clicked.
Writing onClick={handleSelect(course.id)} calls handleSelect immediately, for every item, on every render - and passes its return value, usually undefined, as the handler. Clicking then does nothing.
Syntax and examples
function CourseList() {
const courses = ["React", "Node.js", "Python", "AWS"];
return (
<ul>
{courses.map((course) => (
<li key={course}>{course}</li>
))}
</ul>
);
}
// map produces:
// [<li>React</li>, <li>Node.js</li>, <li>Python</li>, <li>AWS</li>]// Implicit: parentheses after the arrow
courses.map((course) => <li key={course.id}>{course.title}</li>);
// Explicit: braces need a return
courses.map((course) => {
const title = course.title.toUpperCase();
return <li key={course.id}>{title}</li>;
});
// Bug: braces, no return - every item is undefined, the list is empty
courses.map((course) => {
<li key={course.id}>{course.title}</li>;
});type Course = { id: number; title: string; lessons: number };
const courses: Course[] = [
{ id: 1, title: "React", lessons: 192 },
{ id: 2, title: "Node.js", lessons: 100 },
{ id: 3, title: "Python", lessons: 150 },
];
function CourseList() {
return (
<ul>
{courses.map((course) => (
<li key={course.id}>
{course.title} - {course.lessons} lessons
</li>
))}
</ul>
);
}
// React - 192 lessons
// Node.js - 100 lessons
// Python - 150 lessonsfunction CourseCard({ course }: { course: Course }) {
return (
<article>
<h2>{course.title}</h2>
<p>{course.lessons} lessons</p>
</article>
);
}
function CourseList({ courses }: { courses: Course[] }) {
return (
<section>
{courses.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</section>
);
}function CourseCard(props: { course: Course }) {
console.log(props.key); // undefined
// React 19 (development):
// key is not a prop. Trying to access it will result in undefined being returned.
return <h2>{props.course.title}</h2>;
}
// Need the id inside? It is already on course - or pass it separately:
<CourseCard key={course.id} courseId={course.id} course={course} />;// The empty state replaces the component
function CourseList({ courses }: { courses: Course[] }) {
if (courses.length === 0) {
return <p>No courses available.</p>;
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
// The empty state is one part of a bigger component
function CoursePanel({ courses }: { courses: Course[] }) {
return (
<section>
<h2>Your courses</h2>
{courses.length > 0 ? (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
) : (
<p>You have not enrolled in any courses yet.</p>
)}
</section>
);
}type Course = { id: number; title: string; published: boolean };
function PublishedCourses({ courses }: { courses: Course[] }) {
const published = courses.filter((course) => course.published);
if (published.length === 0) {
return <p>No published courses.</p>;
}
return (
<ul>
{published.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}type Lesson = { id: number; title: string };
type Module = { id: number; title: string; lessons: Lesson[] };
const modules: Module[] = [
{
id: 1,
title: "React Fundamentals",
lessons: [
{ id: 1, title: "Introduction to React" },
{ id: 2, title: "Why React?" },
],
},
{
id: 2,
title: "Components and Props",
lessons: [
{ id: 11, title: "Understanding Components" },
{ id: 12, title: "Props" },
],
},
];
function CourseModules() {
return (
<div>
{modules.map((module) => (
<section key={module.id}>
<h2>{module.title}</h2>
<ol>
{module.lessons.map((lesson) => (
<li key={lesson.id}>{lesson.title}</li>
))}
</ol>
</section>
))}
</div>
);
}const lessons = [
{ id: "jsx", title: "JSX" },
{ id: "components", title: "Components" },
];
<ul>
{lessons.map((lesson, index) => (
<li key={lesson.id}>
Lesson {index + 1}: {lesson.title}
</li>
))}
</ul>;
// Lesson 1: JSX
// Lesson 2: Components
// Inside an <ol>, leave the number out - the browser already numbers it.
// <ol> + {index + 1}. shows "1. 1. JSX".function CoursePicker({ courses }: { courses: Course[] }) {
function handleSelect(courseId: number) {
console.log("Selected", courseId);
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>
<button onClick={() => handleSelect(course.id)}>{course.title}</button>
</li>
))}
</ul>
);
}
// Wrong: onClick={handleSelect(course.id)}
// runs handleSelect for every course during render, and clicking does nothing.<ul>{[]}</ul>; // <ul></ul>
<ul>{courses.forEach((c) => <li>{c.title}</li>)}</ul>; // <ul></ul> - forEach returns undefined
<ul>{courses.map((c) => { <li>{c.title}</li>; })}</ul>; // <ul></ul> - no returnTip: If a list renders nothing and there is no error, check three things in order: is the array actually empty, did you use forEach instead of map, and does the callback have braces without a return.
List rendering patterns
BasicOne element per item.
{items.map((i) => <li key={i.id}>{i.name}</li>)}ComponentsKey on the component.
{items.map((i) => <Card key={i.id} item={i} />)}Filteredfilter, then map.
{items.filter(isVisible).map(...)}Empty stateCheck the length first.
if (items.length === 0) return <Empty />;
NestedA map inside a map, keys at each level.
{m.lessons.map((l) => <li key={l.id} />)}HandlerWrap the call in an arrow.
onClick={() => select(i.id)}Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Delete key={course.id} from a list and read the console warning. Then put it on an element inside CourseCard instead of on CourseCard itself - does the warning go away?”
“Change map to forEach in a working list. The page is blank - is there any error?”
“Pass courses={[]} to CourseList with and without the length check, and compare what a user would see.”
“Write onClick={handleSelect(course.id)} with a console.log inside handleSelect. When does it log - and what happens when you click?”
What usually goes wrong
React warns "Each child in a list should have a unique key prop" and cannot track items reliably when the list changes.
✗ {courses.map((course) => <li>{course.title}</li>)}✓ {courses.map((course) => <li key={course.id}>{course.title}</li>)}forEach returns undefined, so nothing renders and nothing errors.
✗ {courses.forEach((course) => <CourseCard course={course} />)}✓ {courses.map((course) => <CourseCard key={course.id} course={course} />)}A callback with braces returns undefined unless you write return.
✗ {courses.map((c) => { <li key={c.id}>{c.title}</li> })}✓ {courses.map((c) => <li key={c.id}>{c.title}</li>)}key is taken by React and never reaches the component. Pass the value under another name if you need it.
✗ function CourseCard(props) {
return <p>{props.key}</p>;
}✓ <CourseCard key={course.id} courseId={course.id} />An empty array renders nothing at all, which looks broken.
✗ <ul>{courses.map(...)}</ul>✓ if (courses.length === 0) return <p>No courses available.</p>;An ol already numbers its items, so a manual number shows twice.
✗ <ol>{lessons.map((l, i) => <li key={l.id}>{i + 1}. {l.title}</li>)}</ol>✓ <ol>{lessons.map((l) => <li key={l.id}>{l.title}</li>)}</ol>onClick={handleSelect(id)} runs immediately and passes its return value as the handler.
✗ <button onClick={handleSelect(course.id)}>✓ <button onClick={() => handleSelect(course.id)}>Best practices
- Use map for rendering, and filter before it when needed.
- Key every item with an id from the data, on the outermost element returned.
- Give every list that can be empty a visible empty state.
- Type your arrays - const courses: Course[] = [] - so TypeScript knows what map receives.
- Split each item into its own component once it grows past a few lines.
- Wrap per-item handlers in an arrow function.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Render React (192 lessons, Beginner), TypeScript (133, Intermediate), and Node.js (100, Intermediate) as three article cards, each with a title, a lesson count, and a level.
Show hintHide hint
One map, key on the article.
Show solutionHide solution
type Course = { id: number; title: string; lessons: number; level: string };
const courses: Course[] = [
{ id: 1, title: "React", lessons: 192, level: "Beginner" },
{ id: 2, title: "TypeScript", lessons: 133, level: "Intermediate" },
{ id: 3, title: "Node.js", lessons: 100, level: "Intermediate" },
];
function CourseList() {
return (
<div>
{courses.map((course) => (
<article key={course.id}>
<h2>{course.title}</h2>
<p>{course.lessons} lessons</p>
<p>{course.level}</p>
</article>
))}
</div>
);
}Render the modules below, each as a heading followed by a numbered list of its lessons: React Fundamentals (Introduction to React, Why React?, React Application Structure) and Components and Props (Components, Props).
Show hintHide hint
Two levels of map. Let the ol do the numbering.
Show solutionHide solution
const modules = [
{
id: 1,
title: "React Fundamentals",
lessons: [
{ id: 1, title: "Introduction to React" },
{ id: 2, title: "Why React?" },
{ id: 3, title: "React Application Structure" },
],
},
{
id: 2,
title: "Components and Props",
lessons: [
{ id: 4, title: "Components" },
{ id: 5, title: "Props" },
],
},
];
function ModuleList() {
return (
<div>
{modules.map((module) => (
<section key={module.id}>
<h2>{module.title}</h2>
<ol>
{module.lessons.map((lesson) => (
<li key={lesson.id}>{lesson.title}</li>
))}
</ol>
</section>
))}
</div>
);
}Show each lesson title followed by "✓ Completed" when completed is true, for [{ id: 1, title: "JSX", completed: true }, { id: 2, title: "Components", completed: false }].
Show hintHide hint
A list and a condition together - && inside the map.
Show solutionHide solution
const lessons = [
{ id: 1, title: "JSX", completed: true },
{ id: 2, title: "Components", completed: false },
];
function LessonProgress() {
return (
<ul>
{lessons.map((lesson) => (
<li key={lesson.id}>
{lesson.title}
{lesson.completed && <span> ✓ Completed</span>}
</li>
))}
</ul>
);
}
// JSX ✓ Completed
// ComponentsPublished courses, with an empty state
Render only published courses as cards, and say so clearly when there are none.
- Accept courses: { id: number; title: string; lessons: number; published: boolean }[].
- Show only published courses.
- When none are published, show "No published courses."
- Each course is a CourseCard component with the title, the lesson count, and a Published badge.
- Key each card by course id.
- Check it with a mix of courses, and with none published.
type Course = {
id: number;
title: string;
lessons: number;
published: boolean;
};
function CourseCard({ course }: { course: Course }) {
// one card
}
function CourseList({ courses }: { courses: Course[] }) {
// filter, empty state, map
}Show one solutionHide solution
type Course = {
id: number;
title: string;
lessons: number;
published: boolean;
};
function CourseCard({ course }: { course: Course }) {
return (
<article>
<h2>{course.title}</h2>
<p>{course.lessons} lessons</p>
<span>Published</span>
</article>
);
}
function CourseList({ courses }: { courses: Course[] }) {
const published = courses.filter((course) => course.published);
if (published.length === 0) {
return <p>No published courses.</p>;
}
return (
<div>
{published.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</div>
);
}Key points
- map turns an array of data into an array of elements; React renders each one.
- Every element from map needs a stable key, on the outermost element returned.
- key is taken by React and never reaches the component as a prop.
- An empty array renders nothing - give lists an explicit empty state.
- filter before map, and store the filtered array if you need its length.
- Nested data means nested maps, with keys at each level.
- forEach, and callbacks with braces but no return, render nothing without an error.
- Wrap per-item handlers: onClick={() => select(item.id)}.
Quick check before you move on
Interview questions
How do you render a list in React?
With map, which turns each item into an element: {items.map((item) => <Item key={item.id} item={item} />)}. React renders the resulting array.
Why does React need a key?
To identify each item across renders, so it can tell which items were added, removed, or moved, and keep each item state with the right data. Lesson 10 goes deeper.
Why not forEach?
forEach returns undefined, so there is nothing to render. map returns a new array.
Can a component read its own key?
No. key is reserved by React and not passed as a prop; reading props.key gives undefined and a development warning. Pass the value under another prop name if needed.
How do you handle an empty list?
Check the length before mapping and render an empty state - an early return if it replaces the component, a ternary if it is one part of it. Compare the length explicitly so 0 is never printed.
How do you give each list item its own click handler?
Wrap the call in an arrow function: onClick={() => handleSelect(item.id)}. Writing handleSelect(item.id) directly calls it during render.
Quiz
- 1.
Which method is normally used to render an array? A. forEach() B. map() C. reduce() D. sort()
- 2.
Which is correct? A. {courses.map((c) => <li>{c.title}</li>)} B. {courses.map((c) => <li key={c.id}>{c.title}</li>)}
- 3.
What does this render? <ul>{courses.map((c) => { <li key={c.id}>{c.title}</li>; })}</ul> A. The list B. An empty ul C. An error D. undefined as text
- 4.
Your object has a stable id. What should the key be? A. key={index} B. key={course.id} C. key={Math.random()}
- 5.
const courses: Course[] = []; What does courses.map(...) render? A. An error B. Nothing C. undefined D. An empty li
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned