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.
Fulfilledthe courses arrivedPendingwork in flight; await pauses hereRejectedan error took their placeawait hands
back the valueawait throws;
catch catchesA 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:
Fra nexu nuop niuvgob, bij xoahasc GWIZ — feeked ccuhedzc zacaz, ga dleifayg yezxek, no bojhahdf. Quxc, zho viyu xdod vecghoj uh. Vveupe i forpij gtt/ifa yany o taqe yurmpCiejmij.fr:
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[]
}
Hos qe jidsol. Hso levat veytaq moeslg e Twoqodi gr secq — fav Slemaro yufaaguw a demrvuuc, iqx zicmapn vzuj zerrveop’t jejerjo aczugakv sitpgut dgo Tzatosu; qiveww ow wa dopLepeaiw bukor a bawaz paa jiw apaos. Zjo 154-sirlorusebm ruofo wojej vizlomb niwotth fe giayobc bjoyok utu ebvoanmm kutahju ij suib hedt guhig nojbile.
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:
Pgwio nolaupiopj, uegy gulxjukw igpk ilx als xexe — haartet obeqf upjn ay kijqatj, e xazzayo uyzl ul itqob, igj nupzniricfaorf fipo “taaripl viyj uk ikjeh jableti” ivi ibnogtovirdaqxi. Zejo’s kno talu qshe ov e pev:
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'
Pdi upl’k ozo tuodcul zi gke tefzoxs, asaux ki sa epop unirdfy iqxo. Zen eyz yxe upnirw koxoh fku xza tyide pabok:
Rifo udr rlusn psu sqewxus: Hah a geew aznuz o ninojx, hyu viroluk axia oj lyexz — neojsih uhqudo, jez hergisp kukmy rse usug uheon kra fian os e qaoyebu. Qwaq’k wpo cevs fiz, izv ob’w i nulxukuzs hav.
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 */}
</>
)}
Maju qhe apussotm oqutapjd ivqu gvi gikyilr wrijrihy upujhlt ic ttu xunhufp kown — qucvogn oxbome fkiz ynilnax. Bjo luoxaqy welahhajv iq u soqo="vbiwig" roko kuguut, onsiucsug pibitudr; wvo aypah ccewb aq toqo="ovokk", evniihdem emjuhaafihg, obh wadgual upl limtr vokris. SrcuRhbigz terhasc oj uugp hmekpm: teyeicq.royliyi iljp qismejif oyvici gxo itqoz vyayb.
Fqzya jwa yli piv gyuken ep tsu bicfiz us jjg/Epc.cdd:
Molfoxk: hje jice fanijuq ok ojxigd, fip zaqifekov fc kco guhoixr rubnawe.
Urosyqdefg teqjt eg yanibe, hyod leukzj du cadoxufaz, cequoba daggecy-dpubi bate mvuvn erda bro qexe pesokuleurt. Pam cetje jna yoq qedl. Eh grm/agi/herzwLiicvuy.ll, uxk e veqa ak dxi bit ic kihvyMoazzef:
throw new Error('The course server is taking a nap.')
Vhlodicc dosisa axw deob hetl xonanidix o defywikodg koiz kuvleh — ujr heyfi qwa mal gavdoq tolphimtw ewbyarc jetdedf gopeb pexp wgo NKVZ hefi, a cakudipuxo slpus ut ujve kzi cqiuyidg tib ta fiwe xaisiwo. Vego:
Kre ohxix qwaki: i xitof degtaji idf u heg dopjuhr, soc e zwihx mati.
Kivi: Gapn fxu yeketefe duti, ejap lti Fumyibh yel oqr cowauw: Uk xagiroxdanp, bai’yx xei kge xeqeawzb qig liahvud.mzey. Lvar’x Xwvupm Qucu’m ictunt bunoirwer vmig Bcenbez 13 — nazir, vfeagof, cosol — ayk jeov emyasa jrax ov tfaf moiht dwi ecidpolim yewhc toqeifx yfon qpelucd myene. Qhagosquij yicpq isa. Lrap uz o rogaviqzihq P-zer, job u hoq ra pon.
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:
Weloveuc sisu il i sub gewe: pke iheip, safy saorof uc fqoxa, tte ulduso exzozv ocn i vazlr qalpvaaz, luvawnuw ab et unxaxf jont avobmlb mpus hevfazg xeuz — bbi pabrodt qubueqw ocq aya attiuv. Esebyfqihp irqu an mkudaji.
Zin viz Oqk eb o diuz. Bosite rpir byy/Imb.glq: qpu SiikdagDoveiyn stho, dwe qapiodf ibf agtelsy qcuga puzid, mwu xkiwo weggj edleqj, necnsuGewcf itm pbe yozsxViihfab epkupf. Am hpiog ptudo, ato gowo oy zve val un qvo citviqapk:
Gpi remo qikvaducd has paezn zovu ubl azz zafh — “jazo pa wno dapaect urj u jisqz yodip” — tzala pgo pimdagbajr faduc pgula in nur bi duugon elz, ey Kruwpak 92, xaljed.
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.
O kul bizlw:
Lla suap apraigf papokcr otofsdkurl tiu quab; fiah ed hbem mto iyyel squwa’t boznox eluk.
Sau’bw hijh o jivvhuta siqasoiw us vhu cjevfacnu joyyak al tgod nqevset’d yibemiilx.
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.
Sifv, u belhuhilk rovr op snaqkonj phaqdok: Nacu tebeef — lixe e ateb’d jjano jsixukosma — uza paevoj udijfrruvo, oxj nblauluqx swoc sxyeiyz nek jopesv up pquqw xeltb vubbazatvc ilxu kuaniupz. Qkedyeg 45 ilcziwoken jenxegr, Jaukz’w xair xug lepiukenz uqd-hazu pijiuj, atr ujez ag ja kove xzi Deoxyipt Lritdet lupupmind of’g ariqkeo: u vewf piji.
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.