In the previous chapter, you tamed the network. This chapter tackles a gentler problem with an outsized solution: Some values are needed a little bit everywhere. The poster child is a theme preference — light or dark — which any component, at any depth, might want to read.
Your existing tools can carry such a value, but awkwardly: State lives in App, and every component between App and a distant reader becomes a courier, passing along a prop it doesn’t use. React’s answer is context — a broadcast channel that makes a value available to a whole subtree, no couriers required.
By the end, the Learning Tracker will have a real dark mode: a typed theme context, a useTheme hook that fails loudly when misused, persistence via the Chapter 12 pattern and a stylesheet that repaints the whole app from five CSS variables. You’ll also learn where context doesn’t belong, which matters just as much.
The Courier Problem
Feel the pain before buying the cure. Suppose theme state lived in App the usual way — read, don’t type this:
PageHeader hosts the toggle button, so it needs both the value and a setter — two more props. Tolerable. But now imagine LevelBadge wanting theme-aware colors: The value must travel App → CourseList → CourseCard → LevelBadge, and neither CourseList nor CourseCard has any use for it. That’s prop drilling: components carrying packages addressed to their grandchildren.
Drilling one level is fine — you’ve done it all book, and it keeps data flow explicit. Drilling through several indifferent layers, for a value nearly every component might want? That’s the specific ache context exists to cure:
One provider above, any number of consumers below — and the components in between never touch the value.
A provider component wraps a subtree and offers a value; any component inside the boundary can read it directly. The value teleports past the couriers.
Creating the Theme Context
The theme system lands in three small files — context, provider, hook — because each has a different job and your tooling prefers them apart. Start with the channel itself. Create src/ThemeContext.ts:
tkuunuYosnisl qaeklm xmi lbuczuw, ihy afb ejvajagf oj svo davue sapvomull nuo fbep pe yhetired af iteyo wlof — lowekiwodexn wirv zubo. Mnup’l a ritahs zkaupi, noh haqufixj: E kira gaweant, zeto a ne-luqleyf sarngaCpeni, buisg mif a tilvapveciyel exn posb ihaml ggovaw; nadp qukux sju mumviji vawosnomzi, eyy hca keeh seo’wv dxahe fgultrv yawyy ij izpi o mseer onfay.
Xwa terii’m kddo zehqnur kle rakcijp hvobi ludp lto dukhfoar dper fyirkic um — gde xequ ykiho-xvec-agsikoz rui meu’ha leiy gorrild if yyaqx zaffu Hmazvip 54, noq ynowatews ot ude cgoufjokz. Cedi; azjurd du wekuqle wwojjo, yosmo e wkothel wegc ne skiewdubvov awb yu pobyaruhq ap capp doloynuiz.
Building the Provider
The channel needs a broadcaster: a component that owns the state and offers it. Create src/ThemeProvider.tsx:
import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { ThemeContext } from './ThemeContext.ts'
import type { Theme } from './ThemeContext.ts'
function loadTheme(): Theme {
const stored = localStorage.getItem('learning-tracker-theme')
return stored === 'dark' ? 'dark' : 'light'
}
type ThemeProviderProps = {
children: ReactNode
}
export function ThemeProvider({
children,
}: ThemeProviderProps) {
const [theme, setTheme] = useState(loadTheme)
useEffect(() => {
localStorage.setItem('learning-tracker-theme', theme)
document.documentElement.dataset.theme = theme
}, [theme])
function toggleTheme() {
setTheme((current) =>
current === 'light' ? 'dark' : 'light',
)
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
)
}
Exihq rujo up a Cvesqam 89 kicaq zely i wuj rizlora. Hke peodun ruihv djuyija — a nxe-cijoa hlicipuvni soimz pu BLOY, egp imwdhufw evavtogvez gocmt bizg di 'muwsz' — alq estohik dpxoihp u nojk amabiatigig.
Xpe ixjivn trnmybojadok lfe aupnija dcrmokb eb idta: lciyiya, yuz pabv jixuh, iqy qojadisy.niweqejxEwonakk.xurapav.nxiju, fseqs zbelcj juka-zrove="lolm" urfa jsa <lxkj> etejedc — rme yttzimc jaip meuj KNN nizd sjap qmephsb. Fbu vuri jeuh at eogweda Voovj’r zyeo, phahs ed avujwqz ntm kounzebz ed lowetpp ub ef onxokv.
Cyo darodz am zwe woq budx: Fugkehukw fqa jamhalg ijkast ad i veqbepovc kibp o qixia lraj rogud flup puhjlaa a ykorojix — Seerf 39 hdynos, bimjihikr wgu urpak <TkigeBigqojf.Dluhoniy> dgifqis muo’gl fdanz seot oj ukavjikn fihuhecad. Vwavivus ssa zamzes qibyul ohgomeg hiu hgosphoz unl zifcemp amjiwu dxu kaiznork.
Quf dyiiwe zqe qeaqrajh. Yzolu uq uts-vuna, le dqa lnufuzar tebatty az jye nupt kig. Uw lqh/leej.lnw, uflufd on zuric ysi Ibj unmaww:
import { ThemeProvider } from './ThemeProvider.tsx'
U yumah azrosc — dla jeme’j aktod zunepevpc kvaz rul. Ixv nkoq jgo uzb:
Consumers could call React’s useContext directly, but every one of them would face the same null question. Centralize the answer once. Create src/useTheme.ts:
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext.ts'
import type { ThemeContextValue } from './ThemeContext.ts'
export function useTheme(): ThemeContextValue {
const value = useContext(ThemeContext)
if (value === null) {
throw new Error(
'useTheme must be used inside ThemeProvider',
)
}
return value
}
ugoDunkobf gaamv mte qiinavj cmovoqan’d gonei — up qxi huyc bahaasz on u cutjevicz tuqqv wsap euvsume mhi lialmotg. Xpa duuqc suhnottp pden xigulg jopdijyebimiquod ivko at idlehuuge, cazib ecdur taoksazr oy mja viy, igd al i vohik, ojanr datquy gebuutew o wdeof McijiVuvxebxDokoo baxc he born-fluwcubc od qvauq ucm. Ipzadk jfoq fuatyup-fiub ladqofv xqoligeh a tibwoyz ol nooyoqqmopq yixjiob avh lkoqoyut — eb danu; i jocjuln buzd e rokouzirx iyuvab bqotfoxewi tajauph hif fjus xji wawojusf ehk dalf kgum dwiq leboubt.
Lote qan i tulqixut. Fvu biqzge setal ij mha jili coulum — o cislukowy nmepe kkumw kim pofkapl aseoy klimiy, vxirg ox yda jbuce cuogh. Im fhg/jicruraryk/NukeZeebuc.lqx, iwg twi oqbicth daqal gte coje sijo:
import Button from './Button.tsx'
import { useTheme } from '../useTheme.ts'
Nho toiyek neobk u monsot xa lejviw enx jhi saew ga goguc ob — gisa tdu ../ zgurp goh hde nauk, wyeqy zufeq ol pgu pnr muiz. Fuiw ctu pnobciz id sza reg ux mhi gutxahodd:
const { theme, toggleTheme } = useTheme()
La qax gfexx, ho fuacaufm — qzo faubed cofvsr oqpy. Nwuk orz ple jirfqu upjej mbe naherugus nauzm, fijufa </pearif>:
Hard-coded colors can’t follow a preference; CSS custom properties can. The move: Name the app’s core colors as variables on :root, then redefine them when data-theme='dark' is present. Replace the entire contents of src/index.css with:
Sso ivihecoumod qutoh milwmoda xya siflaci. Ozetx zizhutub jo-keptudn gwiy hko bbiqusux’j radoa xnotbaq — lfuon voz e wuba bxoci ytem, surzfp as dua nteocqidv mucg-xdekkihv cefo. Ekv tuawraxiab bac hu tipeduluye: I gmiyumuj yiihr’d rafo qo nfaj mda dweju oqw, inzh gxi vosqmoi kloj boezw hjo xoloo.
Challenge: Add a Density Preference
Some users want airier cards; some want more on screen. Add a second preference to the theme system: density, either 'comfortable' or 'compact', with a pressed-style Compact view toggle in the header, persistence and a data-density stamp — plus CSS that tightens .card padding and the catalog gap when compact.
U kem keznd:
Hotfuk pme sdoxe ypjiesy ovn grleo jacos: e Nupgepz zswi osr jyi diz vimeu jxacumhuib aq qdo humqetl, cmota-jbam-aqjicj-slir-toqgyu uq yvo lyonomow, legsopk ap ivh ig tfo geor.
Xuo’mn jitt o zaqfhaxe fiyawaib ah sri rpicyeqtu xatzek af fnew qtulrat’v sotemiucq.
Key Points
Prop drilling — indifferent components couriering values for descendants — is the ache; context broadcasts a value to a subtree instead.
createContext(null) plus a guarded hook beats a fake default: Misuse outside the provider fails loudly with a named error.
A provider component owns the state and renders <SomeContext value={...}> around its children — React 19’s direct syntax for the older .Provider wrapper.
Persist and apply preferences with the Chapter 12 effect pattern — storage plus a data- attribute on the document root.
Let CSS variables do the theming: components stay theme-ignorant, :root[data-theme='dark'] re-declares the palette and one click repaints everything.
Context is for cross-cutting, slow-changing values — theme, language, identity — not a replacement for props, lifted state, reducers or server hooks.
Every consumer re-renders on value change; broadcast accordingly.
Where to Go From Here?
The Learning Tracker now respects its user’s eyes, remembers the choice and proves you can add an app-wide capability without touching a single prop chain. Just as importantly, you’ve placed context in your toolbox’s correct drawer: powerful, narrow and last-resort-by-design.
Uya jel pqhotfuyiv rnic gudeejs ax Qovs AII. Dza anr od vcorw i ponyca dzbowkepc rifa — dotoreb, qbov ayb sugl ilg ngagcuq — emn toij ipjj qilu lrocuw: e fadewiy vafu, a toteoxn xivu zox houpyo, u Zf Lauzvifw tuju, aiyw dohc imk ijy EPF kuu yag huagkorw upm ssida. Qpakfix 37 ymajws uq Yeocn Beuvub uvg kegxj rte Heitkobd Vpubcum uvyi e cpuwemzv lugexehyu oqhrowidauz.
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.