5.
Rendering Conditions & Lists
Written by Eli Ganim
In the previous chapter, you gave components typed inputs. CourseCard now renders any course it receives, Button carries behavior in a function prop, and Card wraps whatever you put between its tags. Structurally, the catalog is in great shape.
The data story, though, hasn’t kept up. App.tsx imports each course by name and writes one <CourseCard /> line per course, by hand. Three courses, fine. Thirty? You’d be maintaining thirty imports and thirty JSX lines, and adding a course would mean editing component code. Real catalogs don’t work that way — real catalogs are lists, and the UI should follow the list wherever it goes.
That’s this chapter’s job, in two acts. First, you’ll give the course data a proper model — a shared Course type with a union-typed level, and one array holding every course. Then you’ll teach the UI to react to whatever that array contains: Render all of it with map, identify every card with a stable key, show and hide details conditionally, filter by level and handle the day the list comes up empty.
By the end, adding a course to the Learning Tracker will mean adding one object to one array — and every pixel updates itself.
Naming the Course Shape
Since Chapter 4, CourseCardProps has carried the course shape inline — a type within a type, awkward and unshareable. Time to promote that shape to a proper domain type that the whole app can import.
At the top of src/courses.ts, add:
export type CourseLevel =
| 'Beginner'
| 'Intermediate'
| 'Advanced'
export type Course = {
title: string
description: string
level: CourseLevel
durationHours?: number
isFavorite: boolean
}
You met a union briefly with Button‘s variants; here’s the proper look you were promised. CourseLevel is a type that accepts exactly three strings — not any string, those three. Nothing stops a union from listing a level no course uses yet; the union describes what’s allowed, not what currently exists. Course then uses it for the level property, alongside the shape you already know, optional duration included. Both types are named exports, ready to travel.
Put the contract to work immediately. Annotate each of the three course objects in the same file — here’s the first; repeat for cssLayout and modernTypescript:
export const reactBasics: Course = {
The annotation flips the relationship: Until now, TypeScript inferred a shape from your values; from here, the values must satisfy the declared shape, and any drift becomes an error at the source. Next, spread the type through the components. In src/CourseCard.tsx, import it below the Button import:
import type { Course } from './courses.ts'
That’s a type-only import — the type keyword you first used for ReactNode in Chapter 4, marking an import that vanishes at build time. And collapse the props type to:
type CourseCardProps = {
course: Course
}
Much better — the shape lives in one place, and the props type reads like a sentence. Give LevelBadge the same upgrade. Replace the contents of src/LevelBadge.tsx with:
import type { CourseLevel } from './courses.ts'
type LevelBadgeProps = {
level: CourseLevel
}
function LevelBadge({ level }: LevelBadgeProps) {
return <p>Level: {level}</p>
}
export default LevelBadge
The badge no longer accepts any string — only real levels. Save everything and check the browser — no visible change, because you’ve changed contracts, not content.
Before moving on, see what the union buys you. In src/courses.ts, change reactBasics’ level to 'Beginer' — one n — and save:
Type '"Beginer"' is not assignable to type 'CourseLevel'.
Did you mean '"Beginner"'?
With level: string, that typo would have sailed into the app and quietly broken every level filter you’ll build today. The union turned it into a red underline with the fix attached. Restore the missing n.
From Array to Elements
Here’s the mental leap of the chapter: The catalog is about to become one array, and you won’t write a loop that adds cards to the page. You’ll transform the array of courses into an array of elements and hand the result to JSX — which happily renders arrays. The tool for “transform every item” is the array method map:
Note: JavaScript arrays carry their tools with them, and three matter in this chapter. map calls a function on every item and collects the results — one output per input. filter keeps only the items for which the function answers
true. find returns the first match, orundefinedif there’s none. If you’d writefor (const course of courses) { results.push(makeCard(course)) }, thencourses.map(makeCard)is the same machine with less plumbing — and noresultsbookkeeping to get wrong.
The functions you hand to these methods are usually written as arrow functions — a compact syntax for function values: (course) => course.isFavorite means “given a course, return whether it’s a favorite.” The parameter goes in parentheses, the arrow points at the expression to return. No function keyword, no return statement — for one-expression functions, the arrow form says it all.
Building and Rendering the Course List
Time to apply all of that. Three separate exports served you well while learning props, but a catalog is one collection, not a pile of variables. In src/courses.ts, delete all three course exports and put this in their place, below the types:
export const courses: Course[] = [
{
title: 'React Basics',
description: 'Build interfaces from reusable components.',
level: 'Beginner',
durationHours: 6,
isFavorite: true,
},
{
title: 'CSS Layout',
description: 'Arrange pages with flexbox and grid.',
level: 'Intermediate',
isFavorite: false,
},
{
title: 'Modern TypeScript',
description: 'Master types for safer, clearer code.',
level: 'Intermediate',
durationHours: 9,
isFavorite: false,
},
{
title: 'Web Accessibility',
description: 'Build interfaces everyone can use.',
level: 'Beginner',
durationHours: 3,
isFavorite: true,
},
]
The type annotation Course[] reads as “an array of Course” — and TypeScript now checks every element in the list against the shape — typos, missing properties and all. The catalog also grew — four courses now, with Web Accessibility joining and the levels reshuffled so the beginner track has some depth.
Save, and brace yourself: App.tsx is a wall of red — the named exports it imports no longer exist. That’s expected, and fixing it is the fun part, because the whole hand-written card list is about to disappear. Replace the entire contents of src/App.tsx with:
import './App.css'
import PageHeader from './PageHeader.tsx'
import CourseCard from './CourseCard.tsx'
import { courses } from './courses.ts'
function App() {
return (
<main>
<PageHeader />
{courses.map((course) => (
<CourseCard course={course} />
))}
</main>
)
}
export default App
Read the interesting part slowly: Inside JSX braces, sits courses.map(...) — an expression, so it’s welcome there — and the function it applies returns a <CourseCard /> for each course. Four courses in, four elements out, rendered in place. The hand-written list is gone; the three-import line shrank to one.
Save and check the browser:
Four cards, including the new Web Accessibility course. From now on, growing the catalog means adding one object to one array. But open the console, and React has a complaint waiting:
Each child in a list should have a unique "key" prop.
The page works, but React is warning you about a real problem. Time to understand it.
Giving Every Card a Stable Key
When a list re-renders, React compares the new list of elements with the previous one to update only what changed — the render-and-commit routine from Chapter 2. But comparing lists raises a question the elements themselves can’t answer: Which new item corresponds to which old item? If the first card yesterday was React Basics and the first card today is something else, did the first course change, or did a new course get inserted above it?
The key prop is your answer to that question — a stable identifier that travels with each item, letting React recognize items across renders no matter where they move in the list.
The diagram shows the two ways this can go. Number items by array position — 0, 1, 2 — and the moment a course is added on top, every position shifts. React pairs old and new items by key, so every pairing now points at the wrong course: It rewrites each card’s contents in place, and — worse — once cards hold their own state, like a typed note or a checked box, that state stays with the key and ends up attached to a different course. Give items real identities instead, and every pairing survives the shuffle; only the genuinely new item gets created. That’s why the index is not a key — it describes the slot, not the item. Anything that reorders, inserts or deletes will betray you.
Your courses don’t have identities yet, so give them some. In src/courses.ts, add one line at the very top of the Course type, above title:
id: string
Every course now owes the contract an id — a stable string that belongs to the course itself. TypeScript immediately lists four errors, one per course. Oblige it: Add an id line at the top of each object in the array, matching its course:
id: 'react-basics',
Then 'css-layout', 'modern-typescript' and 'web-accessibility' for the others. Each id is unique, and it never changes no matter how the list is sorted or filtered — exactly what a key wants. Finally, in src/App.tsx, hand the id to React:
<CourseCard key={course.id} course={course} />
Save, and check the browser and console: The warning is gone, and the page looks exactly as it did — keys change React’s bookkeeping, not your pixels. One subtlety worth knowing: key looks like a prop, but it’s one of the reserved names from Chapter 4 — React consumes it for list bookkeeping and never passes it to your component.
Rendering Conditionally
The card itself still handles its variable parts the long way — an if block computing durationLabel, and a favoriteMessage that renders something for every course, favorite or not. Both work, but JSX has more direct tools for “this part depends on the data” — and the card is the perfect place to meet them.
In src/CourseCard.tsx, delete both computed blocks above the return — the favoriteMessage if block and the durationLabel block — and then replace the entire return statement with:
return (
<Card>
<h2>{course.title}</h2>
<p>{course.description}</p>
<LevelBadge level={course.level} />
<p>
{course.durationHours !== undefined
? course.durationHours + ' hours'
: 'Self-paced'}
</p>
{course.isFavorite && <p>One of your favorites</p>}
<Button
label="Add to favorites"
onClick={noteFavoriteClick}
/>
</Card>
)
Two new patterns, one per line:
-
The ternary —
condition ? valueIfTrue : valueIfFalse— is anif/elsethat’s an expression, so it fits inside braces. The duration line always renders a paragraph; the ternary picks which text it holds. Use a ternary when both outcomes show something. -
The
&&trick handles show-or-nothing. JavaScript’s&&returns its left side if that’s falsy, otherwise its right side. So whenisFavoriteistrue, the expression produces the paragraph; when it’sfalse, it producesfalse— which, as you learned in Chapter 3, renders nothing at all. What looked like a useless quirk back then is the idiom powering half the conditionals in React.
Save and check the browser:
The favorite line now appears only on React Basics and Web Accessibility — absence instead of a “not a favorite” disclaimer.
So when do you use which? A workable rule of thumb: && for show-or-nothing, a ternary for either/or, and an if block above the return when the logic outgrows one line — multiple conditions, multiple values, anything you’d struggle to read at a glance. All three are correct; readability picks the winner.
Note: One trap to tattoo somewhere visible: the left side of
&&should be a real boolean. Booleans render nothing — but0is falsy and renders. Write{course.durationHours && <p>...</p>}and a zero-hour course would print a stray0on the page. The comparison you wrote —!== undefined— sidesteps this by producing a true boolean. When in doubt, compare; don’t rely on truthiness.
Filtering the Catalog
A growing catalog needs a way to show less of itself. The real filter UI — dropdowns, clicks, state — arrives once you can handle events; today you’ll build the logic it will drive, controlled by a constant you edit by hand.
In src/App.tsx, add an import for the level type, below the courses import:
import type { CourseLevel } from './courses.ts'
Another type-only import — CourseLevel exists for annotations, not for the browser. Add the control knob above the App function:
const levelFilter: CourseLevel | 'All' = 'All'
The type reads as “a course level, or the string 'All'” — unions compose with anything, including other unions. Then teach App to respect it. Add this at the top of the function body, above the return:
let visibleCourses = courses
if (levelFilter !== 'All') {
visibleCourses = courses.filter(
(course) => course.level === levelFilter,
)
}
The logic mirrors how you’d say it: Show everything, unless a specific level is selected — then keep only courses whose level matches. filter’s arrow function is the keep-test, answering true or false per course. Now change the map line to run over the result:
{visibleCourses.map((course) => (
With that, the page renders only the survivors of the filter.
Save — no visible change, because the filter is set to 'All'. Now test it: Change levelFilter to 'Beginner' and save:
Two cards. TypeScript is guarding this knob, too — a typo like 'Beginer' or an invented level refuses to compile, which is exactly what you want from configuration. Set the filter back to 'All' before continuing.
Finding the Featured Course
map transforms all, filter keeps some — the third sibling, find, retrieves one. The catalog page could use a featured pick up top — the first favorite in the list.
In src/App.tsx, add this below the visibleCourses block:
const featuredCourse = courses.find(
(course) => course.isFavorite,
)
find walks the array and returns the first course whose test passes. But what if no course passes? Then find returns undefined — and TypeScript types the result as Course | undefined to force you to take that possibility seriously. Try rendering {featuredCourse.title} and you’ll get an error; the honest render guards first. Add this line to the JSX, between <PageHeader /> and the map:
{featuredCourse !== undefined && (
<p>Featured: {featuredCourse.title}</p>
)}
The && pattern again — and notice that inside the guarded parentheses, TypeScript lets .title through without complaint. The check convinced it that featuredCourse can’t be undefined there. Save and check the browser:
Handling the Empty Catalog
One state remains, and it’s the one developers forget until users find it — nothing to show. Filter by a level with no courses today, and the page renders a header, a featured line — and silence. An empty screen reads as broken; a good one says what’s happening.
Add this to the JSX, directly below the map block:
{visibleCourses.length === 0 && (
<p>No courses match this level yet.</p>
)}
Show-or-nothing, so && — and length === 0 produces a proper boolean. Now put the page through its three states, the manual test every list UI owes you. You’ve already seen full ('All') and filtered ('Beginner'). For empty, set levelFilter to 'Advanced' — a level the union allows but no current course uses — and save:
The catalog explains itself even with nothing to sell. Set levelFilter back to 'All' — the next chapter picks up from the full catalog.
Challenge: Add an Expert Track
The catalog’s owners want an expert tier. Your mission, in three moves:
- Extend
CourseLevelwith a fourth level:'Expert'. - Add an expert course to the array — say, React Performance, “Profile, measure and speed up real apps.”, 12 hours, not a favorite, id
'react-performance'. Place it wherever you like in the list. - Verify all three states still hold: The full catalog shows five cards,
levelFilter = 'Expert'shows exactly one and the empty state still has a level to call its own — check which levels remain unused.
Two hints:
- A union grows by adding one more
|arm; multi-line unions read best with one arm per line. - If TypeScript complains when you type the new course, read the error before fixing anything — it will name exactly what’s missing or misspelled.
You’ll find a complete solution in the challenge folder of this chapter’s materials.
Key Points
- A shared domain type like
Coursegives your data one authoritative shape that components import — no more inline duplicates. - A union type (
'Beginner' | 'Intermediate' | 'Advanced') permits exactly the listed values, and TypeScript flags typos with a suggested fix. - map transforms an array of data into an array of elements; JSX renders arrays directly inside braces.
- Every list item needs a key — a stable identity like
course.id, never the array index, which describes the slot rather than the item. -
keyis reserved by React for list bookkeeping; it never reaches your component’s props. - A ternary picks between two rendered values;
&&renders something or nothing; anifabove the return wins once logic gets long. - Keep the left side of
&&a true boolean —0is falsy but renders, so compare explicitly. -
filter keeps matching items; find returns the first match or
undefined, and TypeScript makes you guard before using the result. - Always design and test the empty state — a list UI has three looks: full, filtered and empty.
Where to Go From Here?
Functionally, the catalog is complete: typed data in, cards out, with filtering, a featured pick and a graceful empty state. Visually — let’s be honest, it’s a stack of gray paragraphs that only a parent could love.
The next chapter turns it into an interface: real CSS, a card layout, level badges that look like badges, buttons with states, and markup that screen readers and keyboards can navigate as well as eyes and mice can. The variant and level unions you’ve built are about to earn their keep in the styling department.