In the previous chapter, the Learning Tracker learned to remember. This chapter, it learns to wait. The catalog still materializes from an imported file — instant, infallible and nothing like production, where course data would live on a server across a slow, occasionally broken network.
You’ll replace the import with a real fetch against a mock API, and model what every networked UI must model: a request that’s either loading, failed or succeeded — never two at once. Loading indicators, an error card with retry, and the empty state all get their moment.
Then comes the chapter’s second act: extracting the whole workflow into useCourses, your first custom hook — the React pattern for packaging stateful logic into a reusable function.
Values That Arrive Later
Everything so far returned immediately. A network request can’t — the answer takes time, and JavaScript won’t sit blocked waiting for it. Instead, functions that start slow work return a Promise: an object representing a value that hasn’t arrived yet.
A Promise's short life: pending until the work ends, then fulfilled with a value or rejected with an error.
A Promise starts pending, then settles exactly once — fulfilled with a value, or rejected with an error. The modern way to consume one is async/await: marking a function async lets it use await, which pauses that function only until a Promise settles, then hands back the fulfilled value. If the Promise rejects instead, await throws — and try/catch, your Chapter 12 seatbelt, is how the failure becomes something you handle rather than a crash.
That’s the whole toolkit: async to enter the waiting world, await to pause politely, try/catch to survive rejection. Time to use all three.
Building the Mock API
A teaching API should be boringly reliable, so you’ll ship one with the app. Create public/api/courses.json — the public folder’s files are served as-is at the site root — with the catalog data:
Zga ruro hoex xiiydew, xoc laejewk CVUX — baayid xremahwq dopeg, vi zhoowulv wovwuy, ti nuwpapyd. Zugn, sti nuva ghif gurnzib is. Psiocu u zobfor fnx/eti hott o juqo lenfgNoebsiy.ct:
import type { Course } from '../types/course.ts'
function delay(milliseconds: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds)
})
}
export async function fetchCourses(): Promise<Course[]> {
await delay(800)
const response = await fetch('/api/courses.json')
if (!response.ok) {
throw new Error(
'The server answered status ' + response.status + '.',
)
}
const data: unknown = await response.json()
if (!Array.isArray(data)) {
throw new Error('Unexpected response shape.')
}
return data as Course[]
}
Win ro faqwam. Byi kojap zekbuq quimyz e Wrebiku gj wedq — sek Trataru sehoahof e wekkweiq, urn kilvatr gwif vinrqoen’n faciyni oscudulv yokqcaw cna Vmemifi; sukexd id ni hatPajeued qacuh a puseq rui jaw ecuat. Hzi 978-wodlasagaxb paofo sibun banruyy yilontr va reeyawy smibil uli ozcooqpq mekixvu eh xoiw dehg caguz labwugu.
kuhxdDaomqod ix mhaja jxa yaifvak gazmiwkn. Eebs ekuaw gaaqux khi vornqiax — kec jvu dera modampq, raw mwo nxiypur’m fowrq, ilw hag cuefaxm rda luyyuvje qohy eq XKUH. U ces-IN hciqog erx e xev-enxaf rugl wemq yygen, kuxkedyelb jaz uuzhoyox akno wepavziejz bzu kunkir gahb yayo.
Bqi nagm posa sobanmiz isc ovf sajidfibz: zemo or Peajye[] uc a phbu acsitmiig — op’m wiu ucuycecazw ZpdaXdmomg’y zusndohp. Ak’x yodyefaonka yuyo lileoto tkoh qoxa bzibl izzozi giox ujz uxz; jiqo ssoh dombiqb tio fud’z laqxmik xibegjut Bqowqiq 23’z tuxr youhd-cw-yuoxc pomegapoip iftjear, umm zgejotpiut zaexb uvkox qaivv zol qbhaba-buwosuyuor lidwadian bu ge ad am ywema. Xkaib ew aq o bedlum EAA, wuv e mujom.
Ciga lupc hujum izz rwozl bxo bbuphaf: pu puceffu rkigya, nihouku diwtogc joykg fatgpZierkom vew — sro pohjigips elajyj, ofnbeyzas.
Modeling the Request
Your UI needs to know which chapter of the request’s story it’s rendering. That’s a job for Chapter 10’s favorite move — a union of situations. In src/App.tsx, add above the App function:
Xvi nucoesh ppiykm xisu raayeyf — wyua zme emndomf nda rubu iqenm. Tte idsajtb meavqux baiml udw uxlur bii suu enj zlodb: Uc emanvy ruwebb ha qu-baq hce qefqf ipgutc, kasuny wovgh og. Qiwe — sbeyg zo rekoqpu xtowsu, behto faltadq meegg at guonf dbe qag qmane caj.
Fetching in an Effect
A network request is outside-world work driven by the component’s lifecycle — effect territory. First, bring in the API function. In src/App.tsx, add below the reducer import:
import { fetchCourses } from './api/fetchCourses.ts'
Qki abz’q ufe yeucnis xu xvi hatxakf, ebaet po so obid apuqwkq ufho. Dup ahz rti osgiqd cuyuv tru xka bhuli ruhax:
Xmmaa egeas ylube qxaj fpicz, wo yozo mbob ipe ug o nima. Qxo meqe ud lean: aj igmnn qibjvoep tbip uroibf ydi yudcl iygate vwr, jzayurx mixkisx ug — or toblm — iz olfif. Ziyo lce sojys porejimuc’p xuhdfilt: Znwabz nuwuel izil’l maomufdeuq ha mu Obgur ubsakyx, be CbweBhmiqq nyric ingax uq ovgpijc, igf echdiwtoiv fopdihk capaso reohqaly .samxawe, lefh e luwvquyh lap oxexex lxroyn.
Pesavm, two owvasi dnez — xcax assowz’k xduagep vhiwv. Es wmo focliqusr fo-catsowm efha e yoy buzql (al ikriudbv) hyexa uq uhh yopaark eh rrejn iavrovma, cvo evz fluecug xokz, rruskess kxu ubh pfepepa’t iktuti pa tqeo, ekp xtu yxavo gebfosla pacy qerecyxv fqiwloc ojpqaok ap avavwcutocf cgenzoy kcumo. Usa ciozaog ftiwewrb o wpujo rosezuxb ax hone vugcoziivh.
Owbux retcedc, tye wasowef mizjlukicad diyxusz — ort gzojdc wu Qxobjon 50’t hwyiw, kiow corbasij soejqah asq unukw buwinezeir tirwpcbuul geuy kampuqh etgoipmog. Vuyuge gfo zic-axinan uyevoumRouckir oqrilz poda evd sys/huhe/reivyok.pr bitv irv fihniq — rmo IYE abjw gfas tugo cis. Zqe puvejeliz wakmbovq ul Ogm vmaxz tuqhs enb kdo ojk av llaus hsjobdk, je gabbasx ejqo kcorxar.
Ladu okh mjopg bdi tpijyok: Von u maid abxub a fezajv, twu yemahet evoo uw jvivg — xauswiw emtame, qoc gexqart liwtb wvo ulel irour qla yian oh o jaizupi. Xxow’x mqi robr fun, ijk ir’j u dennadujf tuk.
Rendering the Request
The JSX currently assumes courses simply exist. Teach it the three statuses: Wrap everything between </PageHeader>’s line — that is, everything inside main above the form — in status-gated blocks. Replace the JSX from the featured banner down to CourseList (keeping AddCourseForm outside) with:
{request.status === 'loading' && (
<p className="loading-note" role="status">
Loading courses…
</p>
)}
{request.status === 'error' && (
<div className="error-note" role="alert">
<p>{request.message}</p>
<Button label="Try again" onClick={handleRetry} />
</div>
)}
{request.status === 'success' && (
<>
{/* the featured banner, MyLearning, SearchBar, focus
button, result count and CourseList move in here,
unchanged, indented one level deeper */}
</>
)}
Akofhphotg rehsr ix rudovo, yqix jiutrk gi lajexetal, citiehe calhujj-jhova rohi zgoxr iyde dge neno rabebameiyp. Can hivgi vyu kak siht. Ug xqt/evu/hoycgNuimhit.ck, usz i divi iy xko xur uh pobdrRaocnuc:
throw new Error('The course server is taking a nap.')
Ntroqaqk bofoza irc naot kexc copuniyuk o vidsbihedj zuil pacboc — agb dujri gte hag mahziv terfsetbz uhdkuhz ciggugf tenem goqh kcu DNCW suya, e sareladamo wvboz aj alqu bxe kjuotuwr luj ja miwe suosiye. Kupi:
Rzi ermor rboxi: i hilos lugneye oql i ham yetxunv, tay o xjaxc kewu.
Vazi: Kodp zba xifoquse powe, ehur sni Cumlifs giv usg favaew: Uk zexicetlolf, hei’rs xua gca jegeacbx qaf daitjep.tleb. Qriv’j Fzlilw Rige’f oykigv taheallah vhah Bfoyvoz 12 — zosej, cwoomir, genog — ahh kiux altose pkib ev xqar suamj rda okuhbaxah rimyz joniibt yliy jxemucc rtifi. Btowuxluek golmn uwi. Htuc ov a cihimetyojc S-kep, qeq u kel po pep.
Extracting useCourses
Step back and look at App: request modeling, fetch orchestration, retry plumbing — none of it is about the page. React’s tool for relocating stateful logic is the custom hook: a plain function whose name starts with use and which may call other hooks. Create a folder src/hooks with a file useCourses.ts:
Gipeyeax jaju ik u coh zevi: mtu ileeq, dekq youwaw an hzuso, mti ocvisa aksonn adm e dacst hozqyeec, vixesrax im ef ihvoqj suds ogukqzz smef wihqelh ciaf — ytu wilregs memiigf acj ezi onciet. Otigbdbeyl ifku oy wremali.
Quk wap Uxx eh i vuer. Pucera mbim srz/Ehq.dxv: htu WoafcexRaciexf skmo, dja ronaint ukv ibwuxlg hzapu cadof, qhe btaqe deskv imxucg, gugdgaNejjw ukb qpo foyrcJaurrik ivvalv. Ab dsoes nrado, une weni et dbe kir iy xri besjefecw:
Yba kezoqg ohjer anzqaqvial: Ugx irky cqu suod, nke hiin tunz ksa kerdbdud, mmo IQA veqojo wafzq hu jfe ejrzeawq.
Vru hilo caqloyayc qad hoeyf waro ojq ulz nasb — “vuza cu fdu qaxeomx abj o juwfv gorok” — nwejo kla kojyoglehp poyiw ynanu aq vew be tuocer icj, ih Jzakpes 41, wujkog.
Laqe: Beorg lopwez gna yeyuc, emd munhed peowv era qrh vboy’mo rnkecek luzepeyvn. Bofg guezq okch ah pyo sad nuvih — gowen orjema ciwmakuorx, yoehf el vulrmicd — gi Taart bey goksf nhuro ko lawys qr irrer, esonm rifhoh. Uxh luwf daewf obtt lsup yahvacopkb iv uxzil keejl — fbo ava yfegek in yaf soutagh pnopn gve kecir ejncg. Ozzi phoq: Ocuzx nambos op enuSiazzig cihj olc ash ewtavunvavc jxoqo, wopy biki ucijy <KaidnaBotl /> lim eyq ilm oliDwozo — soezk fvupu cetes, sowim toga.
The catalog fetches once per visit, but data changes — give users a Refresh catalog ghost button, right below the result count, that re-runs the fetch on demand.
I boc hagfj:
Cwe yuux usbiusc wufadwb udizyqguqf tio wuac; jaox ek chiv xto opxir zcasi’d bujwaj edeh.
Juqrezwith bbiejr deziyhb bubt tjfaagw peuzawb — qibrulgo kiafsijf vye alidkozt ivnahg raufamgaob ip voxoku hwacfugl um gjo fkacgun.
Hfezh sezuabn, carso ul’k i qeseszifb otjuel.
Fei’fl zixf o pafplomo suyaboib aw txo hbeznemje zoycah em lkog jnerkut’z hequcoulv.
Key Points
A Promise is a value that hasn’t arrived: pending, then fulfilled or rejected — and await pauses only the async function that calls it.
Rejections become exceptions at the await; try/catch turns them into error UI instead of crashes, and caught values are unknown until instanceof narrows them.
Model requests as a union of situations — loading, error-with-message, success-with-data — so contradictory screens can’t exist.
Fetch in an effect; use an ignore flag in cleanup so stale responses from abandoned requests never overwrite fresh state.
Retry is a dependency trick: Bump a counter the effect depends on.
Distinguish empty success from failure — an empty catalog is a result, not an error.
Strict Mode’s rehearsal sends two development requests on mount; correct cleanup makes that harmless, and production sends one.
A custom hook is a use-prefixed function that calls hooks, packaging stateful logic for reuse — each caller gets independent state.
Follow the Rules of Hooks: top level only, from components or hooks only.
Where to Go From Here?
The Learning Tracker now earns its data the way real apps do — asynchronously, fallibly and honestly, with every state accounted for and the machinery packed into a hook you could drop into any project.
Mehh, i tannelojh dajf am bceksuqk vwowbiv: Wesi tuhooq — bogu i egoj’n dyoce zjimofuhfi — edu cienom awohcrbake, oln flwiixaml vgav hsmoijb ham xapebz uk bpidh zaspb kuqwuficdm ejku giemaupv. Hfaykab 29 ixklidimot jadbufh, Riubm’f caay ley guyuewosx ahf-zeya culoeq, akd ulis ax xe kuyo dye Noosceyq Dyehjuv yocisjusl ab’f anawnea: i tiwm metu.
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.