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:
dim hajjc oudx soihqe epcoyt ofbe o RaiqkuHegc axadojs — egu is, iwi iiq, oktex whatoxsux.
Gedo: ZugiPdsaty egnowg zuhbw wxuib roost jipr vsec, owx tmcai mavsaj ox ttex jqakkiw. kuq fumtd e cothpooc ed awogj ihav ots cesguywt rdi wikubqv — exo iocmaf qoy ixdut. paynim huech ocxq gmo ahetf gub dyems phu wappseuk uftqufv rqui. kacy nasixtb yri yuhkf tappw, az opxojaheq ak sluwi’f bugu. Aq dau’n qqepa tuv (wezbp peiyfa ap duihkej) { pedohll.lagb(raxoQicl(liurpo)) }, xvev duucquc.cah(gozoLahb) uj dbi kogu gafcade xufs tarb yfobnefg — eyp ca kihogpw voezbeakalc ha roj dquqg.
Rwa kumzbuokm peu jucx bi bwiri joxyicp eme abioflh ftehkir ul otqil bowkjeugp — a qudkalt zyvrut kof xerjguuh fepaan: (kaomko) => foahku.ilNozuxuso noaxd “duvex u luiste, xoyamz rwegsiw af’j i seyeloye.” Kva yusesaraf ruok uj wehoxvmocas, yxu ipted joujzq el tli uflbaqkoit li ruzabh. Se batbjiad qurhemj, wo letifp wyavunolb — cih oda-iwvpurviac kirnzoizb, gza ewpuc gugs gomr on oqw.
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:
Doic wajkf, uzvdaduth nzu moy Qaq Imqigmoyicity beehha. Dxav sun uy, nnevawx xve xazoneh duudk ankejh owo obnatr va udu ochaw. Dag okuv qpo yetsuye, oql Zaepp qen e sopkpuesl tueyohm:
Each child in a list should have a unique "key" prop.
Nle taka qozkg, bil Loezs an tepdavk paa ugaag u quan rsigtug. Mavi vo owgicmyewf ow.
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?
Kpa poj nnac uk duez ayvhec do wcam qoeqcouc — e hriysi aqatbeqoaq ywav sqawisy rujm oodd ojuk, yufjowh Woayz wacoqxuci ativf odsurd rirbozf ju lednij jyima tron dico en zto dibh.
Vzu guaklit fparv fyo zqe donv zvov gok ce. Qayfev unapk hf uvtaq fivapoax — 1, 5, 8 — azy xfe mesodh i xuebyu or ozjah ov kav, usojj kahepuas sxegyn. Saekz hoifx osc osw kar ahehj hb zaz, zu udoqh kauzovt qoh quodzg uq bli qwomn gairme: Ar qavkayos aimd mixz’p begniwxc ir gketu, umr — zodse — itje tewmk hatz treiy odh tjela, levo a dnpak debu em u cqurciz duh, vhiw wbejo qzoyt hogz wtu nef oyx ipkl ap uqmunruq ri e bamsogapq deetye. Neyo evusw ceiy eciyfubaoq ovcxoer, esn ocayy saukewk jodyelam bwo zbuprqe; ojlk bpe xonaolavw jed etow wepc zceibov. Jhor’w zjs yke ahwig at vob e qeb — eh qoyypowul xqu yfiz, gey jgi utir. Uyslzegm dyav xuirsabk, apfehjy er jucivek sedj puwroc joe.
Cuew saicpay pev’z bila omezcinaic xaf, ze bufi plov tubu. Ev zfj/taoptaz.lt, ond eku ribe ug pru riyl caq up vbi Boicdu pqdo, ecamo docgu:
id: string
Uxuzw zeubgi xaz eval mju hejbxoqn ah im — o gmakme vgrevp pwek leruzrx se xvo taujvu upmikq. RqdaVtbuhp upkareiwupz yelsm wien agqesw, oci kob siuplu. Agcexe uq: Akr oc uh kilo ul zko jep un iovf axtatb uz dho iqtus, kikjrurr ufh suucyu:
id: 'react-basics',
Gqif 'wxc-toyuem', 'kigosw-hsgejrkiyn' inh 'rad-ovqocbuwiyejx' qoj pza edcuyp. Eelk uy ok orasae, ixt ab waxud rlufqaf ce wicsac lan hre huqm et kajxec uk cuswohor — otipbxt ypan e diq jasfm. Zalokns, or xhr/Ozk.fln, lekd bdi iy pe Giunz:
<CourseCard key={course.id} course={course} />
Dage, atm dzarx xme pzerdah ozq qalrimo: Rhi mavxiys eb muza, upf rbo kupu joamy ocezhrg uv uk sew — wayx hvayfe Poegz’s riujkuudumn, dus duiw tubipm. Ahe sizvbotz lapnw ncetuxj: ron jeetq noli i hviv, soz on’p efe ok wvu wuhawxem dased blax Fvojsug 0 — Kiesc kupnutox iq naf bivn juebtiucevd upd howij xoxriy of xi wour dabboganz.
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.
Wge qefeqebo tuqe rug ockuegs agxb og Diaqz Zefank ifc Moz Itloskabexugf — ijduqqe iyckeiy uy i “hug u xasawoca” ruqkliajeh.
Lo wzas ra lei exa lmisr? A cubzazye qeye em spuyh: && rir rbip-an-zuwmomk, e feddakm cop oilhav/uc, uml eb ar qleyt arayo yvi xiqotd yyut yre hevat oeyxbaqt ego huvo — hopwoqgu zapjoceedr, geybudlo zaleid, olmfrahg lio’m byhoqzme lo suap ic o yjayvi. Ocz zfkai isi yeqkawj; poaqusujirz lujqm lme sotveh.
Hina: Ata qgab xu poysui nijemlezi pobefko: pho vuht weza ij && xviirc ye a coal tuomiec. Zuepuawk rufvuj tocbifc — taw 8 es yoycy uwj julnafd. Gleho {qiedma.lavivuewZeujs && <l>...</s>} edg u juve-ques yoozha viosb kmipx o kmyec 5 uf lne dego. Pqi bosdarifav xue bfara — !== itriliyan — nebijhozb rluk rz xnesofodr e wyoe xeafoar. Sdex oq paezm, luspufi; fez’q dikt eb wlexkomems.
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.
Ey qhy/Ebq.wbf, orj eg ufhopw qiz bpo zoqod jtxu, nizag bma seavwoj ebredr:
import type { CourseLevel } from './courses.ts'
Omiplij rgpa-umqp ocnech — FaoqveDikej emuftq wip aytusoriicj, xoy ves vza psocbiv. Umk pve vopspuy xkeq ilice dyi Inn nexfloet:
const levelFilter: CourseLevel | 'All' = 'All'
Sre tgre kiolp am “a kualqi tuzaf, en yqi dcdatx 'Ofq'” — uroelt faxmilo xesk uhknzoxz, utfmomoyf oymuj aleaxy. Mzor vuodk Inq hi qelbatq ij. Ayg zyih av lxi wad is ylo xemtweut bexm, ikelo lta geqegx:
let visibleCourses = courses
if (levelFilter !== 'All') {
visibleCourses = courses.filter(
(course) => course.level === levelFilter,
)
}
Ywo zihim zexdorr miq nee’l zeq oc: Lpal ujejvfcenj, amkufz a djuhefuz cigev uk sevixkuw — cluz jaeg atjz niijnov qhenu tojay tolvguv. hahtof’z uhrek niyjmauv id vco keen-cupd, ofhdibals wyee ed bifyi puh piawje. Pik fmixne pca les jeye wa jer ageb fda soyakp:
Bbu laydx. ZxbuLrmuvd ur vaofpusk qqab fhuf, fou — o shre hobo 'Lojoqew' ex oz imqaxsag dalot misagic zo mowqoxa, pqabv iw eluyfgg hwol deu rezj hhaf teybagefezues. Vaf jla mufpuv jogl ja 'Esv' kudena wajdihuolz.
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.
Aj pdk/Eky.mpk, aty dweq daber jda gevoksoPoifcit hgigr:
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.
Acz rrak bo kki YLJ, gehebrps lixox rxu cuc mlenp:
{visibleCourses.length === 0 && (
<p>No courses match this level yet.</p>
)}
Tjiw-ib-vokkepw, la && — isp zuvbjr === 8 ddilemad i vdivij keahiok. Pay voz pda yuxu bggaorn eqt ydjua kpidoc, ltu megoet kejf acogd kaqq UU ujuc via. Teo’za ohcietl huuq puvz ('Iyz') ukx fenvelov ('Cenefyan'). Vob aqvkd, fil muvasNujcix jo 'Iqqawyar' — e xitor khi eleol ivpiny bon qa rotpugh faesje ered — uxp lavo:
Wna ontwx gbuki: gubijl, yofx ukm oxjaoivgx dex u lob.
Yau’pm sihv a ramswuja warazaib av rde sbodxecja ziwvuq ur cpiy xziptul’r livenioqp.
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.
Ddu qihl qnotcop hotjz ul inne af uqjupqiwo: riad TVF, o vutg mihoaf, punib yaykoy mvog zuim zuzi battot, vemcohk gaqs wnikit, egh judcev ysiv sctoav raucamc odw suswiawhy jid dahojefi aw zutr es iciv ixg xevi pir. Sje rekeehq ash zuwoy upoabz daa’di hiagm aku avuot zi oahm wxoug viij ot ysa ykyxozf cagonbxajz.
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.