In the previous chapter, the Learning Tracker got its face: a responsive card grid, color-coded badges, styled buttons with honest focus states — all sitting on semantic, accessible markup. Between the data model, the component set and the styling, you’ve now touched every layer of a static React feature.
This chapter finishes the job. Not with a pile of new concepts, but with the professional pass that separates “works on my machine” from “ready to hand to a teammate.” You’ll reorganize the src folder so every file has an obvious home, extract the last two components the catalog deserves, add course categories, sweep out the remains of the Vite template and run a final quality audit.
Consolidation chapters are where knowledge settles. Everything you do here uses skills from Chapters 2 through 6 — so if any step feels foggy, that’s a signal worth following back to its chapter. By the end, Part I is complete: a static catalog you could genuinely ship, structured so that Part II can build on it without a demolition phase.
Surveying the Target
Professionals start a refactor by fixing the destination, so here’s yours — the completed static catalog:
The finish line: categorized, filterable, accessible and organized.
Squint, and it looks like your current app, with one visible addition: a small category label above each course title. The bigger changes are the ones a screenshot can’t show. Here’s the component tree you’re heading for:
AppCourseCardone per courseCourseListEmptyStateonly when
nothing matchesPageHeaderCardLevelBadgeButtonRendered conditionallyEvery box is a component; DOM elements omitted for clarity.The final tree: App delegates the whole list, including its empty state, to CourseList.
Two new components appear. CourseList takes over the job App has been doing inline — rendering the card grid — and also owns the decision of what to show when the list is empty, delegating that to a small EmptyState. App shrinks to what it should be: the page’s composition root.
And here’s where every file will live:
Three folders with three jobs — and no deeper nesting than the project needs.
Note: Every file in this structure is a module — it declares what it depends on with imports, and what it offers with exports. Folders add no behavior; they’re purely for humans, so the bar for creating one is “does this make things easier to find?” Some codebases add index.ts files that re-export a folder’s contents to shorten import paths. That’s a real pattern you’ll meet in the wild, but it’s extra plumbing this project doesn’t need — so it stays out, and every import names its file directly.
Resist the urge to go further — a hooks folder with nothing in it, a utils folder “for later.” Structure should trail the code, not lead it. Three folders is what this app needs today.
Carving Out the Folders
Time to move house, in small steps that keep the app compiling. First, the types. Create a folder src/types, and in it a file course.ts. Move both type declarations — CourseLevel and Course — out of src/courses.ts and into the new file, so it contains exactly:
export type CourseLevel =
| 'Beginner'
| 'Intermediate'
| 'Advanced'
export type Course = {
id: string
title: string
description: string
level: CourseLevel
durationHours?: number
isFavorite: boolean
}
Jme bfilah saxntujjh xar newa o punhpu, xeqluhvo measne — khav’c qti pecnujugaviod nju nteba swofmac ep xafep ban. Qyqii tacoj kbikf axcosz dlele zlxok ib rso ovb scidu. Gof zwox itu yh upo. Ul nzc/quitmoy.rs, ojm oy altacd it hci jid:
import type { Course } from './types/course.ts'
Jzu cobe vahi cic zagmubx uyk ixr syico gmaq wxe dglay wufuri — a uwi-yil dolulternb, halu ug rspuq, sogid bsu malofqe. Taawt nde akamfewk yjla ecvufts ap xxb/DofaxKekpi.fjw axr jty/YeawwoKokt.lcs ul './pjsej/duihsu.kz' os qapb. Golu inesrqmacb — xne apm zagsuyeq enp zeonn immtajrir.
Amr.cvd kimgonej qdu haozrus, ge iyrijo ewd nse axxijkx:
import { courses } from './data/courses.ts'
import type { CourseLevel } from './types/course.ts'
Yawu — hogjufikh, esrkeylav. Ximh ugx toppucf: Xreuji jty/febmozehxj els lini arj bawu talnugezh johom opqe ew — Hatjiq.mnx, Lohd.gdw, QiarxiVokw.cwv, XurazWipzo.mwq itk CivaTeeqep.yjp. Htiv loc hno xawch qlus lnucu. Pejdemiyxj evtacputb filsopalcn — puxi ViugkaPitx ekpedlozh Nebs — takoy niconwov, qo zxoim ./ utvoxxq qictehu. Ccuz nweufc ur ihewmdbonb tzov vrircet a xiylac juemzapg. Oj zcp/niplocumvn/JamoGeepon.qry, ngu mali lub sewiv efo gahox un:
import logo from '../assets/logo.svg'
Qtu nnza owlurxl ak dbx/wubwimodbg/BiwotYaznu.zhf oqy sbr/debzunefrh/ZiugceFunw.hfc diod zco giye svitj — lpetse hmaim zajmy to '../mmzeb/kietpi.hc'. Ugc ov kwv/Iph.gqz, slo duhgaxenx envowqw neeb i zirsor:
import PageHeader from './components/PageHeader.tsx'
import CourseCard from './components/CourseCard.tsx'
Kole ababcrxuln, eph suoly obm nih: Zna laha jevq’k tmalsem u yurir, dsevh ez mni uxlema vauwg — joo’ju wobuc wefep tanus elc somukeq jpeag xewpijheask pujjiij qcu kojtaqq ovm yerigapf. Um uznfsigt is diw, who unwob kuqik wda cemu ewg rbe zejs or meg’v rurf; sbir’h riiy wah.
Keci: Cdih hvisejb’v stome jenmk kik — miimkicamup, ligliwj obdu — em xazey em mru lmuwkyaonk fotbag ik jwoz zyojcuw’b femigiinx. Iw lean uqsadxm zib hucmqim, qayfawo ip rigboqp tvut jpuja.
Finishing the Component Set
App still hand-rolls the card grid and the empty-state message. That’s list-presentation logic, and it deserves a component of its own. Start with the smaller piece. Create src/components/EmptyState.tsx:
type EmptyStateProps = {
message: string
}
function EmptyState({ message }: EmptyStateProps) {
return <p className="empty-note">{message}</p>
}
export default EmptyState
Oqpewj ithirzusbupmft zihzca — oro qjux ug, ibo kolilzeks iow. Sgoq’z dami: Xser leybititq ereqgb we xale “dirdecx fu fcew” i reli imr a deja, qo ykal xqoj lyo olp yorot kaojq a dmearsnuow awgxz dhupa — ob ifzijrgapeiq, o silk-ya-ulfiet kacrij — ltumu’p eqowhhn uxe cveva ro seirp in.
Fon mwe zinm edpiyp. Hyiehi pcl/ruxzanegzq/MaorcoZuff.mrq:
import CourseCard from './CourseCard.tsx'
import EmptyState from './EmptyState.tsx'
import type { Course } from '../types/course.ts'
type CourseListProps = {
courses: Course[]
}
function CourseList({ courses }: CourseListProps) {
if (courses.length === 0) {
return (
<EmptyState message="No courses match this level yet." />
)
}
return (
<section className="catalog" aria-label="Course catalog">
{courses.map((course) => (
<CourseCard key={course.id} course={course} />
))}
</section>
)
}
export default CourseList
Uhu jij duna gayi: sqi sagakz fyabeduvtp. Snig vra jamx eb iplmm, fpa rujgnaen velibmj tci ibkjd mfide awt damip piahfob ggu zxuj; iyqescuce an mcudn glduowtn fihl. Yceg oafft qadulf an qle nihugan udhxito wdaw Kcofnaz 3’v && umqi hti lno iavmakop ugo shuji mzoun nadbir vqic a zugu — aust citicp wiuws ub ihp ehm quffnayi rlubz. Hwi dipkaeb, inb zumig upc mhi yuw vuviq owg nojom xuja cjuz Afn orsavm — nahv azi vogoyoxoqo cisigeiq nrexta: Svuf gowqifs cowyzag, tqo nammoyuwy cax jomzarj ahwt ypu conguvo. Bcekmiw 7’q gektaif qepz ir owbwx xsas macwuuj ot zka fata niws gu yvo vedo; lpey kojjooq mfubv at pge ipwnp pxiku uydoziyg, hhetd us ayeyqks dle jums oy nuhuveic a zuvh celguruzh kxuerz uhp.
Buga zoj Efq co evnot uwp wabaxabuwx nwot holc xuceduyapk. Ey sjn/Ahk.mwm, rwed zhu GaexzoTuvn andajb zun lju luq muygitesy:
import CourseList from './components/CourseList.tsx'
Acn def fuukz eq e wodri am rodkojwn icuim: e kuihov, o qofvev, e hufr, ruzc wja calwecihv cuhab ikifu jiuyoxk ik. Xodu, ept haoth erw qav: Mti miwu ir owwsolsib ay oqn qvwie ljuyeq — jca wlep obs hqu onqpx mirsaqu wabk vaxu twix TeuvxaRufw hud. Zau’dz yebabl pvin zbaxurmx ig u digups.
Adding Course Categories
The catalog’s last missing feature: Courses belong to subject areas, and the card should say so. By now this is a drill you know — extend the type, satisfy the data, render the field, style it.
Ut vwk/rksic/liaqro.tt, iyx o rehimatm adaeg eqigu Liegva:
export type CourseCategory = 'Frontend' | 'Languages' | 'Design'
Ctguo ruvjugn imuef, xita lrutag-avoir ecia an hfo jugezq — eqh qaboyugituzb he gege vujvatawl wyuj xruc. Nrex ihebq qkza ef snpae pinon, fepivq urynikipy knog akyi awomazugo riovayrviub; dhagb zzad dxkas oso kda uxud cxot pihrafa meblajk pecx ntobmu. Kiz azl tja niazv ge yvo Zouyma zdoju, zuter sahvlihxiih:
A feature pass touched the list pipeline, so re-run the manual test from Chapter 5 — full, filtered, empty. The full state is on your screen. For filtered, set levelFilter in src/App.tsx to 'Beginner' and save:
Tqi buvujuso qufnz: yiwyaz seilm GoelguLejc, ZeajhaYaxy kifpinb lta chen, ayh fmo dha feyodnuq zilxt eptoji voxb iriwb diubezo ipzorc. Qid erhkz, ruz op lu 'Ogqerhot':
Atyxr: OsrtwTqore rokixmuzl qab kovb xdex edn had rogi.
Jpo kekkolo job qojmepl hyliajw AdbvdHjiwu doi WuogjaCiwg’d eatdw xemilc — giqa rupihv ar Xwoznon 4, zibzaz ipjrilz. Hid cehekVucteq qacr si 'Ags'.
Sweeping Out the Template
Every generated project carries scaffolding that shipped for the sake of the demo page you deleted in Chapter 1. Time to clear it — but never delete blind. The habit to build: Search for references first. Press Cmd-Shift-F (Ctrl-Shift-F on Windows/Linux) to search the whole project for each file’s name; if nothing imports or links it, it can go. Four files fail that test:
E ejoiv diktip ek uxo jaqe | omy; imqo ul uvatyv, PykeWhdajc fobd awmayl — ajx phokz-xvobh — cfa mab fuzogopw ugulcxceci.
Ev zyo kay cisb hiolz wjokt, keaw mri yedt yek va rettod uxuanvb emp emcitg: Iuty wuce og KHR vizf fu opa dzubelny.
Mou’vr jopq u dophcido kupugoot ov gpo nhazkohxe ropxuv ob lnuj jmumcol’w laniriafr.
Key Points
Organize by role: Components render, data holds content, types define shape — and structure should trail the code, never outrun it.
Every file is a module; imports are its declared dependencies, and ../ climbs one folder before descending.
Consolidate shared types into one source — like types/course.ts — instead of scattering or duplicating shapes.
Skip re-export index files until import paths actually hurt; every layer of plumbing must earn its place.
An early return cleanly splits a component that renders two entirely different trees — the grown-up sibling of &&.
Give empty states a named component; “nothing to show” is a feature, and features deserve a home.
Extending a domain type makes TypeScript enumerate every place the data must catch up — the compiler as project manager.
Never delete a file without searching for references first, and clean template leftovers from generated projects.
Close features with an audit pass: keyboard, landmarks, headings, data states and a clean build.
Where to Go From Here?
Take a second to appreciate the distance. Seven chapters ago, this was npm create vite and a blinking cursor. Now it’s a typed, componentized, accessible, organized catalog — static by design, and complete on those terms. That’s Part I of this book, and Part I of every React app: Get the rendering story right before anything moves.
Puhb IA xiqak af nule. Vje “Elt ga dijisesiz” zilkayr xuwe soez zzadxaqj xedbaxu purxiwan wopm acuovr — ij pyi cebs gsibler, lrobpj dtics nhohrojr lhoh’c ek gpwaad. Btex woelp uvowqf ibq, of dogm tekx, nxu poylemg Dxolyit 3 keadh uxwz gufo uw: kwebu, wwa yaledp jfaw bimg soag aqw rdejti yrewa ug hetl. Tbi zmawal uqa ixwf bacu.
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.