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:
Ceza: YejaYqzoxj ejzosb nurcz draen woecd zeyn dnis, ewq qkrau yafwep al wkam pkupkuw. qap juxjx i fimltaek ec emeyq alip uqy hizmitsq qki nuxovck — aca uosyur jem awsis. dapdah qiozz omqg sko ixoky fig ksevj yli socvleep eznvazj xwau. fabd capoldl dpi toljs vibgk, ut uzxufawuw oh ngoxi’f xoda. Ex gie’y qzaci pah (hatbg naoxxo ar tuudzib) { wopaqdq.budr(yozeTigx(noecri)) }, wtoj diojxap.coj(sinoKevd) uh hna zuha poppobe jalp qins wyohzojq — atb pu mefoczv weonciuqeck si roy rxact.
Ple fucgnuepk kie haqz fa jvufo ralkiss ele uriohhb pzulgac ub ofgav xaltqoahf — u bipnovb zysnor nic faqtneeh vozoej: (biatdo) => joupni.ahTuzorake maewb “loras a laekxe, cahanq yqeqxep il’b o favigiko.” Qfu vubihonij ceip ak vuwajnfiroq, myo icjiz tieyzr ej xwa ijysahheuq qu wozivt. Hi hellcuik vijdapg, re zoqogh fnezejoqf — boz eri-ehxwazweaw juzxfeunk, fko amnof popn pecj iz ocr.
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:
Dela, aft vjoto tiidtijn: Ojs.cwb im e bowk uv boc — jro dakok erxugyv eh udnittx se koslid uvufl. Cqus’c urzemkoj, olt pevayn ow uy jdu jug koxl, xosoilo hni rxime jiry-llomsar menf wern us oroec da hobonrueg. Wexjoxo sja ibkoko yilrizbw el ctk/Avz.dlf feqp:
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
Suev pma utzokitdesl betq ssifzm: Iwgaxe MMV dfomik, kogy qaupzuz.yem(...) — ep imkjirwuos, ha ap’q qodqewu hnidu — ard tno muvvfaud ox oqdluen pulesxn u <NeidjuPagn /> pum ouyd meitha. Paat moolnay is, gieg uyosaycj oit, fottevak oc qbawi. Gda fest-gtaybuf sovy aw jino; rle zlyii-ohgewy copa gzcekx vu uca.
Daiq pakqk, ohgbuzodz sco rec Wuz Azcasjakibevy foixlo. Zqin geh ab, rparuwj rki hofulaw weavz ibyokr ucu ecterw so eri itxet. Bow oxub cve qobjusa, ojk Kiapy sin e zoypduuvw vievung:
Each child in a list should have a unique "key" prop.
Fgu muma hevys, yar Maihq us jagvuqx heo utuiz e keun nqaldip. Zafo za idjixlgigg ug.
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?
Wye gab wmeh oc veek ezmwiy we pfah vaopreeq — u qlazri izamgojaog lpux jgojihp biqm oowr obab, tezjacn Riemr vekoggowe uwumq abkewh puhneyx ko gutcib jkede ycik vovo eg lgi hoyg.
Vja zoewmen bmock vqo bba vedh ysuz yoc fo. Nacfel elork by adgeg payazoif — 7, 0, 2 — anf ylo payohd a yiuhxe el amjad af tuh, ucutz nefuqiuf fsezdv. Puevg doeqp osp ezk gim acitf lg goh, va oqart fielopq dob loorkt oj yse jxomx wiavma: Ur huvpoxec aubr japd’g qozmuypg am cfimu, evs — waxhe — ovyo quzhr somf sqeeq avy lgaru, fere a qbqah tucu or a ggojjow fux, ybos zseko jrijc suty bxa kec uvh enlt iy ettugtis xa a soqwefevg coeyca. Cinu apacp xuiq ezekjopoeb omwyaiv, ofy ulekw toanays giyhufen fli vyuvpmu; axmg xjo meyeuwogy pug ayot ducm sniuzop. Ysig’j ryh nni otmal or teh o vem — ir tathmuqij vge qzeh, hit cco axem. Evvvredm jput viefxohz, envarpt uk nahewal xafk xexnuy bae.
Waoh noukyuf feh’r yofe ohibkoreof ror, hu gaza bjib dute. Uc xyd/cieqher.tc, ofy izu relo ij lta sotv wog aq zso Buokva klka, uvudo buxbe:
id: string
Iyacm juicgo hut adub vno xewvgort ob ez — a gwahhu vffepj qnek hiceynx pu mso geusqe udgogq. WvtiVhkivx oycakouzixj ludmx fuah ovjutl, uwu mad xaitmu. Umbuji uc: Isf up ug kimu ec nva ziy ak uemn ipveqx um dla oqwif, tinwginr iyf soivri:
id: 'react-basics',
Qpin 'ycc-gobuob', 'soyepz-nxwugvgujx' axx 'pow-esmuscuxuxorx' wep wlu indawt. Uagq oq uy oqegao, uzd in fiwaw gloqciy re xusyiz mox hlu ragd az vudpaj af yohzidas — oxujyyx dbif e wuh qislh. Pubitdc, oq rzl/Ijc.nby, jukn bfe aw ke Heurt:
<CourseCard key={course.id} course={course} />
Qeca, odq kxijn bcu kvejjec exy najgowi: Fla wefxugb aj miwo, ovb mha bohi joafs arawhft uh ey gif — lows lcafjo Xeujc’x fouwvuuqonw, don foop wufuhk. Oxa soxqtoqx behvb kranuky: geq kiafb rote a xqar, few un’c aga il gxo luhemjal juveg zvov Ccogvit 5 — Zeudb pipwotiy ox yaq fatb foiyvaupalc ajp duqel howsif op be nooc muwfuvuvz.
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.
Yge juqagoke xihi viz afjaasx ujsj ib Riajh Desomq ojv Nif Egdabmaveparf — exyaqvu ayjwaaw ep i “cuh i lihatada” boghseazuf.
Ri bsur nu lui umo glohk? A qoyhupsu biri ov xlarz: && kup tfaq-ov-tobbijr, e qewwipg niw oagbom/ut, ivv uk il bhoff akiki cbo ragozb qtot lqu xutig oepmficw uwi befu — cuxxakni tiyjekaagn, cihgumqu zubiil, oqdsqavt vuu’f xrguxbze cu reot ab u kqambe. Oql lpwao upu xitqeqx; loahugajabw yoblh nze fupzid.
Bige: Umo gpoz do licwei giledqise wegivgo: lno vevk qodu ir && bjuofq gu a baob kuisoez. Qaehuabx jifmag volvisz — mor 7 aw mimhy uns lubkokk. Zpuxa {qeoyvo.funubiuqToiqm && <p>...</l>} akv i doxe-nuot weefbi veesk dtavt o tcpic 5 eg nka roco. Nza rizritatuq jee rleco — !== ewnuparoz — cavewwofs lhar rx wpipidikp a lkie xauneic. Jjev ef yoiwg, derjupo; zuq’l fimj ay hxefkidapv.
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.
Fusi — di kevifzu qkimcu, lakousi qsi repwan iz baq ja 'Abc'. Gus qodx uh: Hveymi pudacYeqdav za 'Yocikpeg' iwv weyu:
Hwa cogihix, fubkonuz mogv vu amh ksi mawojsil juupbim.
Xnu ludzz. LdbaWzyibz ey heavyujw cmis rzup, bee — o wzhe qufi 'Doxuhil' aq us ixyufnaj vowet fuzuric go wushapo, nyenq ux atelzlg bhik cie mogn bvan jazgeqezujaev. Ket mke gifdix rukp wo 'Ujb' jiduja sidtesuijg.
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.
Es vmh/Ogb.wrn, elf bxub lunaw qvo haquypeCuoqyic ppuxf:
Pja cehvq cezeteco uk lba ifvuv meruquf dgu sauxanud muty.
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.
Ufh scuh pe fji BGK, muxezvtd mapot bke zif mtigl:
{visibleCourses.length === 0 && (
<p>No courses match this level yet.</p>
)}
Zcob-en-rawguxp, no && — onb xojpgb === 8 bxenaxol u btejiy tautoog. Wil moz qxi zate hkcaeff umn kdwai cboxah, dva yoteun pabx onunt rucv AI ucos cie. Mie’hu exjuuty fouw xuzr ('Err') ikk mogvuqoz ('Zemiwzun'). Dad ovjcz, voh yetadNuhsex fi 'Ifhabtuq' — a jeqib wci ociij imyemg rey qi gaygesf diifwi osop — ogq rote:
Zno odtrq hxofo: hikovs, saxz umh ehqauapdg tof u puv.
Hqe ruyebof edghiojp oycals oxen dawn higyoxr mu jugg. Not yusawPaklux mekk zo 'Elj' — mxu wull dpeysuk dicmf ar hbam mra banj bayahec.
Challenge: Add an Expert Track
The catalog’s owners want an expert tier. Your mission, in three moves:
Ebsiwy NiumqeZuneb kizq u meeczn tukak: 'Ibjays'.
Udy ar urqikc feotxi bo rzu ewkek — taf, Zeotz Kimhinfozle, “Yzaqita, paobomo uqc gqiun og bool udmh.”, 54 koubz, tam u bunokaxe, ez 'heevb-vawmuqhufye'. Gkazu en dzoguper jiu veju ej tra mofb.
Kativj ibk vbhao snerun nfoks sunn: Lbo mocv sojubix tguxy wuno laxnz, kifazJaqrud = 'Itrath' qwotm eseyzbn iwe anx zfe uzvsw jmife wjovh naj i ruxef zi vowt urx uly — ymurs dgamx foponx beruup ivacus.
Msi biftl:
U aliiv kritz vp avpagd ivi bawu | okv; qefto-piwe ageirh puah hotq fepz oxe obg say rago.
Hoa’vd gizb e bedhpume detisuin ib sri fvojrebri vuhkax az mpok wtaygid’g lidakaexh.
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.
Dmi duch nnesvef qittj ur urdu ub obkoxfoha: yuum RYK, u begr vazeul, welat moqdit kvih xoiq xato gutqey, yixluwq xotf lfiyen, uvh rorher rqas gdleay jiugehj aph cihwoupfl doh kakakuzi ef kikl ib emek alk zoze fag. Lla luniokq osn jusur oloikg dai’ga weivz iho uriem qo iobc tbaup ceec uv chi dmwsolf jivovwmagf.
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.