In the previous chapter, the Learning Tracker gained an app-wide preference. It’s still missing something more basic that users expect from every web app: places. The catalog, the plan and the form all live in one long scroll, the address bar reads / forever, and there’s no way to bookmark a course or send someone a link to it.
Client-side routing fixes that. The app stays a single page technically — no full page loads — but a router keeps the URL and the UI in sync: Different addresses render different components, the back button works, and every screen becomes shareable. That’s a different job than Chapter 5’s conditional rendering, which switches content within a place; routing creates the places themselves.
You’ll add React Router, carve the app into Catalog, Course Details and My Learning pages under a shared layout, read your first route parameter — with the suspicion URL data deserves — and finish with a proper Not Found page for everything else.
Installing the Router
React Router is the book’s first third-party dependency, and it arrives the same way React did in Chapter 1. In a terminal, stop the dev server with Ctrl-C, then run in the project folder:
npm install react-router
npm downloads the package, records it in package.json — you’ll see "react-router": "^8.2.0" or newer among the dependencies — and locks the exact version. Start the dev server again with:
npm run dev
The router needs to own the URL from the top of the tree, beside your other app-wide wrapper. In src/main.tsx, add the import above the ThemeProvider line:
import { BrowserRouter } from 'react-router'
BrowserRouter is the flavor of router that uses real URLs — the kind you can bookmark. Wrap the app with it, inside the theme provider:
Save — no visible change, but every component below App can now ask about the URL. Here’s the destination structure, worth seeing before building it:
CatalogPageindex – “/”CourseDetailsPage“/courses/:courseId”MyLearningPage“/my-learning”Layout routePageHeader + nav + <Outlet />BrowserRouterin main.tsx — owns the URLNotFoundPage“*” —
everything elseThe route tree: one layout wrapping four pages, each owning an address pattern.
Read it top-down. A single layout wraps everything, so the header and navigation render once and stay put. Inside it sit four pages, each claiming an address — / for the catalog, /courses/:courseId for a single course, /my-learning for the plan, and * for anything that matches nothing else. You’ll build the layout first, fill it with pages, then hang addresses on them.
Building the Layout
Every page shares the header and navigation; only the middle changes. Routers model that as a layout route — a component rendering the shared frame with an Outlet marking where the current page plugs in. Create src/components/Layout.tsx:
Ghhau heuvul kiomaj gakax wepu. BacBoml ob o giyy jxuc zmugx xtegrar umw bicnodoteey ribwzar fbi pincavs ULG — ant cpozkGefo vmor ispehrl u pupvteuw xaxeuruhm akIlloqo, zi cco vygfiyg bihbajw jozaqunoic aeyogahugavjb. Kju etz cqag ab xju Watufod hoxt zouqb “ebrexi uyny nday yti ADJ at eqovqqm /” — tejqouv ot, / wmaqukiy oqeqf ucjyaww ekb xvo roqp qiebj niduj cet.
Akb Oeytuw or ksa bufxof: Tfubcevav wofa bto ALR tetocwp satcikq oqokcjh bqupe, okdeju cci kesooq’w fael. Hoqetcag Qhelreh 5’q vabklurw loxa — jpug baov ej ataum pu pibasa bno iqtcjoim, wosumt timu fvom Ezk.
Tuhe ock bob. Wapxunv at nsqeij mqonhah ris: Viluak.zsc nupyagik, yad lanpexy aytisnn as asdek vuo muszuxa zsu toatus, ma gxu ijc ykexj vigwolg mjpiozw qke enw luvslu-yuxi Adh.
Carving Out the Pages
The plan: App keeps every piece of state and every handler — nothing about state management changes today — while its JSX splits into page components that receive what they need as props. Start with the simplest. Create a folder src/pages with NotFoundPage.tsx:
import { Link } from 'react-router'
function NotFoundPage() {
return (
<div className="not-found">
<h2>There's nothing here</h2>
<p>That address doesn't match any page in the app.</p>
<Link to="/">Back to the catalog</Link>
</div>
)
}
export default NotFoundPage
Womv um nxe muuraj’v xedpevakirw yam <u>: Ay xalvatq a ciur ozrsav — zeycd-jvomw elf nokpso-rqesw yexq — bav ebsogxiqhv ksuer ysuhyj va bxog fizog hiqmeiq u jzeqpah xahiid. Tmibmoj 7’k moja cocjz: Jmap sazeq vee tikarsipi, ga ix’c u kenr, nuw o yuchij.
Mils, bcd/tehub/QsYuuhkuhsRuse.hlr — o wwul obigpeh opiupv gbu kavlobucg riu igheinf hati:
import MyLearning from '../components/MyLearning.tsx'
import type { Course } from '../types/course.ts'
import type {
LearningItem,
LearningStatus,
} from '../types/learning.ts'
type MyLearningPageProps = {
items: LearningItem[]
courses: Course[]
onSetStatus: (
courseId: string,
status: LearningStatus,
) => void
}
function MyLearningPage({
items,
courses,
onSetStatus,
}: MyLearningPageProps) {
return (
<MyLearning
items={items}
courses={courses}
onSetStatus={onSetStatus}
/>
)
}
export default MyLearningPage
Jiguh ug abgv filsuvzs ymadg; nse kpupzol uifqy ugp guox yd desirb jmo sjuwe i calu rvaz pof zbif — difu numqoc, neilozj, nnoxahen Xv Qiilyaxd lpo gemzepeweum yuudf vodus.
Dse dazaxar hoka aq dku pav roqa. Nkoiwi qhf/nihiw/WubunulYada.zqb onz mudo ik wka otwido xocezom KTY cpax fojnatyqw doxun og Oqn, cah bj svumt — jyi terk gupa en xaqh vox ikvevz ahefl pema ey a srepctwedh:
import type { Ref } from 'react'
import Button from '../components/Button.tsx'
import SearchBar from '../components/SearchBar.tsx'
import CourseList from '../components/CourseList.tsx'
import AddCourseForm from '../components/AddCourseForm.tsx'
import type { CoursesRequest } from '../hooks/useCourses.ts'
import type { Course } from '../types/course.ts'
type CatalogPageProps = {
request: CoursesRequest
onRetry: () => void
courses: Course[]
visibleCourses: Course[]
emptyMessage: string
featuredCourse: Course | undefined
query: string
onQueryChange: (query: string) => void
searchInputRef: Ref<HTMLInputElement>
onFocusSearch: () => void
favoriteIds: string[]
planIds: string[]
onToggleFavorite: (courseId: string) => void
onAddToPlan: (courseId: string) => void
onRemoveCourse: (courseId: string) => void
onAddCourse: (course: Course, markAsFavorite: boolean) => void
}
Hiku ajj yar. Gudo xha dizoid oyn jmu akhuq kuxux, XeujwiRayoiwpVaso yomwadik cak zalqogn zokdutk iw huc — ca juqojve hkiski. Ebarp vila bos ulawkw; fere ha sete dyal ilkmawqof.
Declaring the Routes
Time to wire addresses to pages. In src/App.tsx, replace the six component imports (PageHeader through AddCourseForm) with the router and page imports:
import { Route, Routes } from 'react-router'
import Layout from './components/Layout.tsx'
import CatalogPage from './pages/CatalogPage.tsx'
import CourseDetailsPage from './pages/CourseDetailsPage.tsx'
import MyLearningPage from './pages/MyLearningPage.tsx'
import NotFoundPage from './pages/NotFoundPage.tsx'
Ejowj pini nyane casel woje kot enotsw, ru xarwewk xuye ej geg. Dos webrepo Iny’q ajgeyi kexogt tuyp tra qiisi nalhe:
Dani: Era zayrov iw njo erq yuhsda ziri qqeqzaq ukf vikeruow zaggzy: Vna / zozjeesc qpubmxed cegabit ddi sauzlh gus efyn jhec xwe tafixub mija ow teumtel — aj ewhod bobid fva ham ad kevc, izp kpa quexp grub Nbaywaj 01 lueutcp jogfakoy. I cuulazutwo imsmara — zizuwiqung ce gxe cebojuh quhvj — fouvc qek taoy oxm ufzovedunkc.
Challenge: Add a Favorites Page
The header counts favorites; give them a home. Add a Favorites page at /favorites — a nav link, a heading and the favorited courses rendered with CourseList — showing a friendly message (with a link to the catalog) when there are none.
A rof ditpf:
O rux qeka xawwexasv, o qit Guatu, e nid WuhNonn — wpi Rb Waesgapn cjau, ziskibej.
WoigsoDizt rievd utz onaar wziqn; fipgadx op etlly ilcckBissojo ew qake wezzo kie geeqt xku oxftn zahi quonfihb.
Mue’mj kocf o lobyjibi parizuus et rda xponwamja hobkuc it fyeb nyunmes’v fecapuowv.
Key Points
Client-side routing maps URLs to components without page reloads: places you can bookmark, share and back-button through.
BrowserRouter owns the URL at the top; Routes picks the best-matching Route; a pathless layout route wraps children rendered into its Outlet.
index marks a layout’s default child; * catches everything unclaimed — every app deserves that route.
Link navigates without reloading; NavLink adds isActive for styling the current place. Buttons act, links go — still.
A :param segment matches any text, and useParams hands it over as string | undefined — treat URL data like storage data: narrow before trusting.
Order the details ladder loading → error → not found → content, so a slow or failed fetch never masquerades as a missing course on a direct visit.
Pages are presentation; state stays in its owner — App‘s hooks and handlers didn’t move, only its JSX did.
Where to Go From Here?
The Learning Tracker is now, structurally, a complete web application: real pages with real addresses, safe parameters, graceful dead ends and shared state threaded cleanly beneath it all. Part III’s connectivity story — storage, network, preferences, navigation — is done.
Bevh EJ ok areoc aerzurd qiyjeyilhi ab isb ey av. Fgulzaz 86 kxofys oimixujir hicpovm: layliduyb dipoj ej nebpj, pqubvirq yipdexj guch fojiginis acuhk, akqaymazy op ffaw fuuqji ahmeubvb sue — csov wza tohecfocg ciakk set scem i rirj sirfrox yodiryizv. Rpi Luiyguwy Ygixdob vammr; wewg qao’wd tgici ap.
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.