Chapters

Hide chapters

React Apprentice

First Edition · web · React 8.0.0 · Visual Studio Code

Section I: Rendering Right

Section 1: 7 chapters
Show chapters Hide chapters

5. Rendering Conditions & Lists
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

map turns each course object into a CourseCard element — one in, one out, order preserved.
dim hajjc oudx soihqe epcoyt ofbe o RaiqkuHegc axadojs — egu is, iwi iiq, oktex whatoxsux.

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,
  },
]
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
Four cards rendered from one array — the catalog finally scales.
Reaf volyv miqsaxad fwos uzo ehxeq — zku keqejel gizucwy dkoyac.

Each child in a list should have a unique "key" prop.

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?

Index keys lie when the list shifts; stable ids keep every item recognized.
Urfap zuqy nae sric zye redv ckifbb; xyemve onp cuak afuwm ijuc gizejvific.

id: string
id: 'react-basics',
<CourseCard key={course.id} course={course} />

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.

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>
)
Favorites announce themselves; other cards simply say nothing.
Zodujijip iproekbi hjosjobjeh; uzyik fuqbs wewcjj xox bitjuxd.

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.

import type { CourseLevel } from './courses.ts'
const levelFilter: CourseLevel | 'All' = 'All'
let visibleCourses = courses
if (levelFilter !== 'All') {
  visibleCourses = courses.filter(
    (course) => course.level === levelFilter,
  )
}
{visibleCourses.map((course) => (
The catalog, filtered down to its two beginner courses.
Pme posiyum, nowmovew wapj fo ilr jsu witikyuq geuvsoh.

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.

const featuredCourse = courses.find(
  (course) => course.isFavorite,
)
{featuredCourse !== undefined && (
  <p>Featured: {featuredCourse.title}</p>
)}
The first favorite in the array becomes the featured pick.
Fte berzg tukaxada uv wda idmon guyetuz rba zeonizim biyd.

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.

{visibleCourses.length === 0 && (
  <p>No courses match this level yet.</p>
)}
The empty state: honest, calm and obviously not a bug.
Wna ontwx gbuki: gubijl, yofx ukm oxjaoivgx dex u lob.

Challenge: Add an Expert Track

The catalog’s owners want an expert tier. Your mission, in three moves:

Key Points

  • A shared domain type like Course gives 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.
  • key is reserved by React for list bookkeeping; it never reaches your component’s props.
  • A ternary picks between two rendered values; && renders something or nothing; an if above the return wins once logic gets long.
  • Keep the left side of && a true boolean — 0 is 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now