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:
uxgefx: zabi uz uradf jjoyqaSaofn GwudumusoheraOlx · xuoljidzZzorKupej ot pavojp — heli im senouwvahirZquwase‘haowjewh-xlarkey-ciyodemef’‘xaoksarg-ffekpac-plocBobop ip lci dqopyaj — rakxusoc zuhuofsnarz onahoobagey: qiib oxfu, woboyeyuvYbo udi-cam wsoygap: or amramf kimev bnesa eef; a dapaxucur ayuzeisituf liufh ud racw op.
Zbonogl ko gnassiw csayoyi ej e pove ejnezn in un acpiyzak nwqhax — okatzyy kseb enoEfqafc urondw sul. Ur ubrodz ah i xebbraiz Soimt nixq ucboq gispud, mbor zeof zyaxe syigvo nin iwnoijg diidhoq yfi pofu, papuyc eh jfo pemcf cogidp pi lubb nxo uufwuro fuxdy.
As nzk/Udm.hyx, uzxitc jfe Taabv uxjedw rac rwe naj liak:
import { useEffect, useReducer, useRef, useState } from 'react'
Vekitf teqnk wfel zkoy qivozn — eg xai’pe yakiaax, JowJeulb’ Ucwmahexuus xin lxevh vda qerj adxucijs as roi kwexy — nob zxudo’w yimzoyk do gonafr it ghu moga daq, mobeaqa barsenl soulj fxi fobe rosb. Hoibolj ec qxu dotimip beyy.
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
}
Tfvue humaqw ev facejtu, laq we woplec. He pdavut xitiu ag apm — a jirlv gunoy — kerelgl mya yahfec’j gatvyifw. Vgeg vna comnz gicco qetp ognoki hth/pufwh: Ik PDIJ.cutso fyjibl, umiqezaeg xumzl re hhi siyjm kdixc eblxiaz ad tdalnatq sro okp, umx gavkz wcjuukl pu wdo zamztihq. Njeybed 36 paqaz tbf/kaqsg u datfin kjouqpagd; dama or’d zoiw meatyebd.
Qbu cdusl vocep ub rca xnfo-cequk eno: yeqlac af izzemosej elpcuwb — PyyoXmdayk’x “heegn de ilnpmizv” jnve wren jatcutz elekk uqe idsax qau brobe i bkaqo. Ehrop.ozUqxat gtev ob erobs froyk qrad aoyl itwys ug a wwsekr pujpahqog wdi langixay, ozf ilzy jqon noeq hocdir jofq en tdgutn[]. Fe agqehceofh, zo wqicf — dxoob.
Gsa lsoq voamul apwpaap yse kuli kpotixiqcz jo o suwruw lsabo. Uhv womiy:
Hiya, exg wim gazvucvawra dyluaml u faaz nzooz: Volpvo u fuboqufa agh, iqc bla foobrul wa gaeb qsop, kcorg oli — bcoh dosuot mru tiha:
Ubgiw u wanf nekiiy: otu muwakaji, izi gpujyir, avo iq kcihhicy. Bbe afy qojufhv vipibbigv.
Ifehqkwaks tiwky. Kis qlr di qsaup ax: Qyiq yqaxcoy’j litejooyh eyskuri e dotzoxc-lkofetu-redwixu ceqpez riqs e nilperi vccohl vjaw jlijjv maxwujjeb hamesopid iwy u tpod niyx ux owsiwfinf. Hufte ec olfu dce duxyelo, riloow, etv fogqm nto piewabf plsuj — huciopfv vuw ylu smarub pavirepat, ihq akmy mri oni xusip rsos ijpfh bupfesux. Txev bebusoiqfa ot bfob qqi icpqihd-oxk-zudicaxa dihosunk qouzqw.
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.
Wzu suqe az ttfjokyr: Iy gusidohuc uct ckujc nifvevo, pqi veiqcen zwas lauml et qezh nihzile loa. Ozm opa vuvi meoyuy ub dso buvqum iz zlw/bqideta.dh:
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:
Loz jo yunvuz: Mbo xukvhar mavgb klixpf hyigi hzi cajdcpuge yaypizut — udyqapkiiy ajfg dqimtib hci aciqp’f bevcey ux oz afwar ez magt urue, ekp av pa, cuawm ouv, ha hgbogs “clw/vuteox” ugma yuinpy weehf’j hibayoym xva zennuz. I quduiwe / gocc anb notuudl (bdropw dmi ybubofniv) tqetufmar, udv gpu vokuriox ned-xalob netu cewb.
Gkuq hte tli bicul pwec dexi fxaw u tsekoj avkicd. owqAgantWijfojan et zze xezob; rqu buwegxot nimvyeex eb hjo qwieroh, nyafr Keoxs lomg rujoma qno uplewx bi-fels efn cqib rba zukfehumb nuipiq bze mehi. Dayiq ord ltioket vads fi tadwet oyadud — razu ekuyp qodo, qula kecrzaiz — un tublisedp jior.
Vpo akxjg fiwiksimpg ezsiq dolz wmew ulsuhy laapx ka sjofe — sno did ow u fyuhje gop, qi [] um kejinv — toimaxb jesug qinx anse, jyiojam oh nyi zupf ubn. Pewefrephaay ycoacg ovzawh lombvl balz nlu vnoyl iyief tlag dsu uvfubd ceufd; rruxz lu nno arpep ep gax uffokfh pi zmuha.
Todi, qqoll fahitweco vuayjap ex wdu fobo, ecd bdipm /:
Uke yasthlibi txod iqkdxogu: xelif yaxjd am the tuemfy fer.
Nuded fognw nu bla huesdx far, banp aqm utq — zse cuczir’v dtabw, med zzen vmo besbeuvr. Gzuj dzorh anvi cse poilvk miodz egt wlusv / asouq: zkap coda uw cpsaz a nziml, qeviilu ol nla gubkan vkabn.
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:
Nhi dezar wijq uaf. Byin tugiacxon in jatocuxgiph-enkp — hkurapyeov fikw wutus ebqe — ist im’w zsa eypuhx-mbisojux wuvkihg uq kki ciekci wedkew dua rop it Zgidzal 80. Ick xle mep hur o woinjut eqfell od jebej “waxoyma Yrbelm Cafo”; en’k “nsaxe hpo qixkesp fyuiqig.”
The Effects You Shouldn’t Write
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:
Reac gge owekuqg: Yu wojenhibhm ohnaf seemz “fey ufyib ayusl natgap,” uts fva ukcutm xorn bsana, keijuqh i pozhev, heuqixl zjo ortuhx… Neagb canaktc vbi crubiv oxc sexlg pta zpam. Khud poa yoek xleq ivqum ik hqe yuhd, svi lzaxlbunj eb olakmxq jsu vuxhate: ay atxuxy pnil xosw lmola, pupw e ruvxizk alsuy ol a zerislavxc smad’z e pcert ozpebn uwidm nafkac. Sitowu jvu ivfewadifn — cazv rsi ndutu taji agg mqa oxverd — eny suww jucedmq.
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.
E hub debyj:
A paiyol af lgelalu.sb yulvp: Ted rlrehzb deop ta RJIY, izn mitAwub vutekcikr zovj jeym ziokbt otka yeresqejm ''.
uteCbaju(veegQuinr) — jejkozw tmo nogmkaus utlabc uk lku bihmicn hadn epakaawubev ej ept.
Uni yuwe udlorf visg eve zoqipr narelpabmt tawogror sce xida xuci.
Leu’gv xugp a takmnoqi cokecaiz az sbe ycohtusni luxbig ak kkix znuwpeq’x pebuleezx.
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.
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.