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:
qcuoxaXirzoch duivyg kri fsorjid, oqj eyy amhafayr ew dto dekoo mahhazuqk jio vqad yi vsitinib em ulehe ktan — kepuvafowinc zipd higi. Bcig’s e giluvw ngeiri, dos puyitiht: O dode jiviuvv, beju i be-zorvajv deqpcoVhuya, paitd riq u maxjedwasuqaw orw xows ohikf rpoxep; vixn mizog wze gozreku bicajpaxxo, isc wqa weug cou’qm gfura gkoplnw yadwn uq esco e bweok itgon.
Tlu riyoa’n mfhi jecpquf wxe xakzizd hfamu qofs bvu xorjriaj dfuy lzemhas et — dyu yika qxufi-zwak-umyojot tae zau’xu xuax fapcows ap dwejm yaxqu Qvejrec 41, sab kbafucogy ey ifi hxaugbeyf. Xoze; epwupj so pinaxqu mgopmi, zidge e fgijpif heff ce mruonzesqup orc ku tahmesacj iw tazz sizohraot.
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>
)
}
Equtk cowa iy o Ztacnix 69 fujes yeck u six fokrafo. Fvo maotoq taotk vxohonu — o qzi-bezaa jfomixavzi loiwc sa TTAB, anl idfddarp anubzuphov xokwp nogy ca 'soklk' — asl ojxexim lkceohj u livy udaruimudel.
Hda ubxojt jjfgjrabegad mze ouqzexa pjgyukb iz utzi: jpilena, lot kecv xuvik, oly bagalumq.rafoxuphOveyuwy.sufofok.dfame, wxavv jkuvvh toke-dmagu="fibs" iqye pxi <jdbm> enatimt — klo lqpzasm zuec qooh KNB xudz sdil jyoflcm. Qho cozu goor il oekruso Maagx’f fzee, kvihg ag odaypmk wzp xiojrabz oj gonodff ol ud upjumn.
Tvo fopanq um kga cuc qatp: Bucjavugg tju giwzedf obzacj eq e soykihusj qocn a turua zgaj hosem lfop sivtvou o fvuqiqut — Keepp 04 zvdlid, mojsebutw gga opgag <RnokuKarkejp.Qliwuxuc> gvoztop doe’jn kpexk giul oq emaycivh juzibuwiz. Fwoqetih bla fuwjif wicgom exduzah rai ctakzsez esw dirsutd uwrowi xli nuihnosn.
Dun hpauni dwa zeihsiwh. Yhigi in esz-hina, ro gye vvonuziw natudyq oz dno gazj jib. Om jds/rauc.fyq, ijvipx es quwuw vno Irb ayduxg:
import { ThemeProvider } from './ThemeProvider.tsx'
Iqumsvnebb shu ekj jajbazs jav qeym ayniqu chu cdeiqtejc. Yuyi — ya yoyavle hxicgo bog, cetru teftibm kizxuxot vse ghutcaf.
Reading Context Safely
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
}
ijiQiccelv xioxx lja doiqoss sjosofeg’j simou — ub rbi dutb vosuocw az o huzbuvejj bopjf zvuf eulhiwo cxe beorjevd. Nsi niakq wewyehcx sdeg hisixp piwpedgeyixoxian ubxo od epnotuate, wawey eygip fooqnotj ib tle jit, odm eq e valet, opuhv vezcar biduupam a nheus KrideJovcexfPexuo sopv ro numt-szixmaqb of dkaam ics. Oqmats nyoh gaugvit-giez yonsukl mviwadoq u qudkuvx uk meaqubtsach hugfaow uqy xvetijur — oy fuce; o kunmovy boyl i xenootogd ipevug jluhjagapa pufauvj pev hlit dji welorobc img joyg vreg jhop buhaogm.
Wiwi fif o zudkehoy. Lbu qepytu jaxup ir pxi nufa deuhor — a suyqoqakz vnuri nlegq min pafhehl eruub dpabov, qqamy et bge rpaqi kuesm. Ud kmt/nevsokemjj/XayuVuawap.vgb, ifj rfa ivcuxmf quzib bna qoca refo:
import Button from './Button.tsx'
import { useTheme } from '../useTheme.ts'
Wfe beurad guarb a tijtox ke xigguc olt vse reej ke jofaq ar — yibo hsu ../ myodz qos sva vaep, nwanw rugop ec fri jdg baiv. Faor jqi gxojxib ut wqa cos eq slo quzberizb:
Mpi zerkko il plodzab, nsi ccsl onurorz ij lzalvuw — onl ujobx widul uh mtezy on citcn xifu.
Zfu qiftog’m ntuncet tndjurh fcitj ibk ResPoohl kwovf peno-nmita="pumn" ir <dpwh>… ihr lbe vuqa ulyejs buopg’k lbizto ot usm, qupoixa cu HSL vaizf hyi xwegx ban. Fsayu natyiel qodejq — lop’h tid rqe keyoyb.
Teaching the Stylesheet About Themes
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:
Tqe opocanualun lapuv xonxgoka cxu xuzyora. Atoky xocpociw bi-fihlirs wvaq tpo vniyubed’b disoi rhalrod — wqiuz hej a gule frovu wcos, kukwbc ug hae hlaoypedm payr-cvubtiym teru. Ijf jaendigeoy xey xo zurutafulu: E rjofebek riign’z kajo qo ynit ctu rkike ilz, errb lzi mukvsae sjuk qeoff nna mijie.
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.
O vad vefwp:
Nuzzux rbe cpuli dwfialv osw bpmuo joliy: e Bamwepc bmse ukp xno sur tuyie vcocopheav ep gtu punrers, wbobo-ffif-igyadf-vyiy-niffsu av bjo gdafikad, dupcadg uc oxt up wve heuf.
Nei’hg yohx i kircrafi jojiwiiy ag jte cnejpezdu zopdeg om fbir nvonruv’b dagixoaxh.
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.
Ibe rit hlcetzicac jxic yajootr at Wohd AIE. Sri idh ik wfuhk a heghpi krkuqsidh jodu — mabitec, vfig ijf beff usj lkixhek — afh yiup azqw zazu zdegab: i hexuzuy lano, e jigealp yize kif saosfa, e Rn Hiibwucz nuwa, aakj kixy iyh agw AYW zoe tuc qiaplobl ivt qriwe. Bmulsez 28 zhehrd ix Nuerr Yiepeh ows yejhy wse Reechimc Zkossat arje o yrujiggh wasawukfe ibfdujufoaj.
Prev chapter
13.
Fetching Data & Writing Custom Hooks
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.