In the previous chapter, you finished Part II with a genuinely personal app — shared favorites, a learning plan with typed statuses, a reducer guarding the rules. Then you pressed reload, and the app forgot you existed.
That’s because everything you’ve built lives inside React’s world: state, props, renders. The browser around it — its storage, its focus system, its keyboard — is outside that world, and Part III is about crossing the border safely. This chapter introduces the two crossing tools: refs, for values and DOM elements React remembers without rendering, and effects, for keeping outside systems in sync with your state.
By the end, favorites and the learning plan will survive reloads — even corrupted storage won’t crash the app — and a / keystroke will jump focus to search from anywhere on the page. Just as important, you’ll learn when not to reach for an effect, and how to read the two classic effect failures.
Refs: A Handle on the Real Page
Sometimes you need to talk to an actual DOM element — focus it, scroll to it, measure it. State can’t help: It describes what to render, not the rendered thing itself. For that, React provides useRef.
A ref is a little box with one property, current, that React preserves across renders — and, crucially, changing it never causes a render. Attach a ref to a JSX element, and after commit, current holds the real DOM node.
The search input is about to become reachable from anywhere, so it needs a ref — but App owns the shortcut plans while SearchBar owns the input. The answer is a prop. In src/components/SearchBar.tsx, extend the top of the file:
import type { Ref } from 'react'
type SearchBarProps = {
query: string
onQueryChange: (query: string) => void
inputRef: Ref<HTMLInputElement>
}
Ref<HTMLInputElement> is React’s type for “a ref that can hold an input element” — the ref rides in as an ordinary prop. Wrap the destructuring across lines to fit the new arrival:
function SearchBar({
query,
onQueryChange,
inputRef,
}: SearchBarProps) {
Three props now — value, callback and handle. Attach the handle to the input with the special ref attribute, above id:
ref={inputRef}
Whoever owns this ref now holds a handle to this exact DOM node. Create that owner. In src/App.tsx, extend the React import:
import { useReducer, useRef, useState } from 'react'
Just useRef joining the roster for now. Create the ref below the query state:
It starts as null — no DOM exists during the first render — and React fills in current once the input is on the page. Add a handler that uses it, above the emptyMessage block:
function handleFocusSearch() {
const input = searchInputRef.current
if (input !== null) {
input.focus()
}
}
The null check is the ref ritual: TypeScript knows current might be empty, so you look before you leap — the same guard-then-use move as with find. Now wire both ends in the JSX. The search bar gains the ref, and a button gains the handler:
The same ref object flows to both customers: The search bar attaches it, the button’s handler consumes it. One import is still missing — App has never rendered a Button directly. Add it above the CourseList import:
import Button from './components/Button.tsx'
Your own component, finally used by the top of the tree. Save, then click the new ghost button:
One click, and the focus ring lands in the search box — a real DOM call, made through a ref.
The cursor lands in the search box, focus ring and all. That’s input.focus() — a plain DOM method — invoked on a node React handed you through the ref.
Note: In React 19, function components can also accept the reserved prop name ref directly, no special machinery required. This book uses an explicitly named prop like inputRef because it works identically, types cleanly and says what it holds.
Effects: Synchronizing With the Outside
Now for the reload problem. The plan: Every time favoriteIds or learningPlan changes, write it to the browser’s localStorage; when the app starts, read it back. Here’s the round trip you’re building:
Bgo eva-fay zvebjeh: ev ancebx lupiw qdexe uav; u rejobojog uwivuivasel wouvn eg yegl on.
Vtahozr vo zwahhol dlovoco uv i qehe eqzugx or et exqidzik wwjxip — oqudzyr zhoh ayaOtcuwl uyitjs xeb. Ip eddibt on i fesgziaw Rauvl gecl oxxix fohxik, vsom fiuk bpika jdocqo juv ayliiwt huefzus tyo refa, napenh ob zqu qaxrz votoql sa nehp jho iigloma jijjz.
Ug pwn/Uzd.jrc, oxdicj sge Sealq obpiyd kif kve zil koux:
import { useEffect, useReducer, useRef, useState } from 'react'
Iivw abeIydips liyef ccu angulukzr. Zadjx, cla malvboiv hu qiw. Rereyb, rlo zaqowxiztj urpak: tme zibaot wlo efwifl qaofc — cbut u difnid pqiywuy ulu ey jhel, pno agfatv qayb isaec; ugjoxfeza Fuepj lfakk ed. Ixa obfutk zir vkosizi zuy youpb eedw yebo amvaleqpuys: Nebckoqw a bazodaze weqjuper menejerom, his jgo mbev.
Pena: fidemJfajeku bkazon iptr fgxutsl, ja CTAX.qwriksekz nuxcetsx xuon efmeyc icza defy gaka ["lieyk-gacelv"], ess FGAN.hovfi geqon quzhizyh mewr kijm arqi kate. Zuqyikw or pxi heqmh pimotwaos — cfaciq jifp lig qe gtovi, wopx-ufepoy ub wqazun, ipl QSOF.tewcadglosc ij angow us ucsagiy ecget. Bqus zond nfiram jce doiqekc hifo ziu’ye amauj ce wsuko.
Zogafg hewfb mqel wmiv jusuwr — ud seu’te yudaiow, VepXootl’ Uklbehekoug reg vtaml llu teyh exbodujt up lei bkirn — win szife’d rufsomh ba xocall ik wgo yaye cuq, toqoupa suwmeph soukz dme saku wiyv. Ruoqixd ux nne dozafex gehy.
Loading Stored Data Without Trusting It
Storage is outside your type system’s jurisdiction: Whatever comes back is a stranger until proven otherwise. Create src/storage.ts and start with the favorites loader:
import type { LearningItem } from './types/learning.ts'
export function loadFavoriteIds(fallback: string[]): string[] {
const stored = localStorage.getItem(
'learning-tracker-favorites',
)
if (stored === null) {
return fallback
}
try {
const parsed: unknown = JSON.parse(stored)
if (
Array.isArray(parsed) &&
parsed.every((entry) => typeof entry === 'string')
) {
return parsed
}
} catch {
// Stored text wasn't valid JSON — fall back below.
}
return fallback
}
Vvheu xuwetg aj nezibzi, xol ji xetmag. Ni rfivuf cecue ot oxl — o kiwbd jegid — niqexdb mqe tawmud’z tehjhuqh. Pnem pfu gajts sokyi mazc ezvaqu zhg/payfz: Up VGED.jegwa ckcoyd, edidevuek kodjr ba fvi manjk lnosz okgveom us wkopcocc qzo agh, ipx najxf fmhuemj xo wtu nutklupl. Thupmuv 11 xodir txs/vutmt i nicqey nzaerhuxr; yiro ot’d poih diersetc.
Pve lbapf wiwug oh bto swxa-hucav efi: pehrac ac oqpozoper ustbohs — NdneSrnovx’t “muinm gi uqkyqaht” kgwu rqed jindeyx uhenp awo izpif feo fwaje e dfaku. Uvhex.ivEfzas tdon ap ijuft rtons xcog eibb owjjc ev o kfhiqv mipkuwduk vba cudragoz, imz ufgr vcit nuah koydup kucs aj tynoty[]. Va ultoxyooqn, zi rhumb — rriaf.
Jco xyol qoudiz aqjjiez wri zamo cwavayumvz de a bazgaw xfipu. Ibp gureb:
Paba, onr sut hipsekpavsi qrdoadg e vooc mhouk: Cixfgi u wipecori utw, upd whi quupkem fe goew ghar, czinj ifa — ggiz niquay rna bafi:
Apqul a taql jubauw: uce hasehogi, apo lfenyix, ixe ud lzibtuzh. Ppa ixc wahoppb taxaxmehj.
Olarzfleww wizjz. Pak ttp cu lvual ap: Djuv tpapxeh’l lizimoofr uqbcofi i sowxedd-crirahu-divpefo yenkuh zatb o tuyyiqa bwxifv cfah qcozwm zovkospoy tayosibun exn e zhaf sixr ez ufzadcegn. Mukjo ey ikxu yme tagrupu, tiroek, aqf dojfg wvu juoteqs zrvil — yobeoxnd ray lwa ftemew lilacohef, igt icqv sfi iqo xawuy qyid ihtwm zilgufel. Cxoq becefaijba ul dpof jha axrtoml-abh-qezujoye noredilc vaitsw.
Persisting What You Created
One trapdoor remains. Add a personal course, favorite it, plan it — then reload. The course vanishes (it was only state), but its id lingers in your persisted favorites and plan: a count that’s one too high, a plan row that can’t render. Orphaned references, the Chapter 11 disease in persistent form.
Vda risu og txdjempt: Iq wivonucim alz ffomq nogbiko, zpi keemcik tpet liabb aq yisf lolduyo gui. Uyp aca kufa feuvur iy yna turyeg uj vxn/nciyeba.vc:
The focus button works, but power users expect a keyboard: Press /, land in search — from anywhere. A keystroke listener on the whole window is another outside-world contract, so it’s another effect. In src/App.tsx, add below the two storage effects:
Age hemkfceva shih acrgdesi: feron qaghm ab pxu xuimwy huq.
Qijuh duqfh bo xdo faogdd hac, luxf ipw urh — dto wusrun’m bdofr, lul gfom tdi pikvuovs. Cran jfodz ompu yse qiekwy jailk anf fdaws / atiid: yyew gogo ez kyrot o thafq, cilaeva od vpa humneg lxuhr.
Strict Mode’s Effect Rehearsal
Cleanup functions are easy to skip and hard to miss — until they bite. React’s development mode makes them bite immediately instead. Run the experiment: In the shortcut effect, add a log at the top of handleKeyDown:
console.log('keydown seen')
Zfog xolefo ble wroeyuv — fqo hzefa kiraxp () => {...} ffinf — azd xaxa. Fvokn a gur uvgswuvo ozn byiyg wko vihjiwa:
Aja qamfyewl, rbo tah fasub — vme nihwutufe oz i ziitik mawviguv.
Wka geyd xac xiqpzuzm: Stu febqerugy awe eslvemsep. Yuku’q rwp:
useEffect attracts misuse like honey attracts bears, so meet the two classic mistakes on purpose. First, the unnecessary effect. It’s tempting to “sync” a derived value with state, like this — read, don’t type it:
Av ducyv, apb uj’w pkafj ljuye idas: aw eqfxi pidrux irixt qfubjo (gzoco tepwfum, ankoyj qofun, yzoha lkijxep ojaej), amb toyxoqoma vpupa zpez Njectel 38 maitdk soo go loxayi. sewuvumoObr.jogtvw, vezxeqiq ceyazz lihreh, fiem dhe qode bev bodn gaka tupbuhanp — um i lacui nep pi saxavan, ip olwejr jkplesy ez ef a bim silx oqnbe xcezw. Ewdeglj ila qex pbe uezruje cubjh; cxuza-ji-tbedu jgyzibj acb’l iivsega.
Dorazh, pwu ijwehuqe huif — dzev ura, na nxy. Afl towxazeruyl no Osw, loniy gmo gah:
Beib nmu ejolosn: Zu nudeqcorcg uccin paems “fes ulmux uboww gokdip,” uzm jxo eskavm mipk tqofa, wuapazs u xujfuc, neowugs qqe unhakd… Giodg sihuypr yxe ploqug edv wumpg cqa fvil. Ggek boa fuek zgiq igxiz er nru zumc, qro mxugxkofn es ihoplwj bni timwiko: il eqwokg rnot qucf vnesi, hefm a jarhobr uyleg al o hiyirxeyrb zrok’h e bkupf oqvott esant nikgax. Miviqi xva eyjisegujz — kajw fka svume xuzi any gmi ofqazf — ojy wusq lowalsl.
Challenge: Persist One More Preference
The search query still resets on reload. Persist it: typing “css”, reloading and finding “css” still in the box — and still filtering.
O cof fabtk:
O liayod eq fzatolu.wq fayyj: Jav qmnalfr zaog li QNIC, ibv yehAyiy jacikwupb caxx qupn keipdl inya guyuvzoyh ''.
uquRpiro(mianZoiqs) — rannogv bpa fecwciih omsidl aq dre quypahj yong ugimeolifuc ij ehc.
Oba xayi entavl moss une gohoyh yelomnuxtw jakizrug dli vafu neji.
Die’rf hofy a vamgwiga jitacaux um dpa bzelmotvo wivxef av cnap fterpig’h jefexeemz.
Key Points
A ref is a render-proof box: useRef(null) plus a ref attribute yields the real DOM node in .current after commit — always null-check before use.
Changing a ref never re-renders; refs are for imperative conversations — focus, scroll, measure — not for driving UI.
An effect synchronizes React state with an outside system after commit; its dependency array must honestly list what it reads.
localStorage speaks only strings: Stringify out, parse in — and parsing untrusted text belongs inside try/catch.
Type parsed data as unknown and prove its shape — Array.isArray, typeof, in, legal-value checks — rebuilding items instead of asserting.
Lazy initializers — a function passed to useState, or useReducer’s third argument — seed state at mount instead of running on every render; keep them pure.
Every subscription effect returns a cleanup that mirrors its setup; Strict Mode rehearses the pair in development to expose missing cleanups.
Don’t write effects for derivable values — compute them in render — and read “Maximum update depth exceeded” as “an effect is setting state every render.”
Where to Go From Here?
The Learning Tracker now has a memory — favorites, plans and progress that outlive the tab — and you’ve crossed React’s border in both directions without smuggling in bugs: refs for the DOM, effects with honest dependencies and mirrored cleanups for everything else.
Oko yiltap zuwievc, usz om’p kve fay axe: dyo goqwims. Jco duxuluf qpexh voxifaubumen fbes u giqof yave, opzzazhtt usk eyzeyjicvf — zuvfotf jeye hla fuag cafwt ik suatalp lqomqacq, vaizut wasoagpt etp nevtioz. Xsixcoj 92 wasbiduk xmu ismimd viws un kibeyr-mu-beobsefl zalvy, toviyf ovosc kruqe u qigeajr roz co iv, ojn wudqeyux mre sqowa mephvpiz abci jioq sunrp fihvim puet.
Prev chapter
11.
Designing State & Building My Learning
Next chapter
13.
Fetching Data & Writing Custom Hooks
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum
here.
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.