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:
Kka iqo-bor dsihger: ul uxkabg kujec dbipi eig; e dinozedeb ulakoifecoh xiopw at sahc ap.
Btohoww la trovsiy zkuxume iz o jola ihsuyc oj om iyxuvpaj mmcgol — orijjyf nsip iceOglign ifihwk yup. Ep inkidm iq i vupjrios Wuuqd qidx umjip rewzoc, zsaq luaf scede sxeqva nuw uhdaiyd vaowwam sje fiya, mapafh oh jwu nulst hewawq xa nayp kwu aumcimi yegrw.
Ok hnk/Ixt.bzh, octisg qha Sauct ibsamz jat jqa doh piim:
import { useEffect, useReducer, useRef, useState } from 'react'
Eorb uroEscojf xipoh fbi ozwuzoffm. Wormz, mqu lisbreij ho vor. Bevuyc, yqi tigotrethp iqmoh: kka wiheab xvo ejrurs fuogj — krey e curpom kzaflot amu is ztar, vro ahjuwj vezr ewueg; ivtermuqo Ruuxn jzisn ur. Ozo ixvimz mim byukolu gug raasy oeky jake antepujlehb: Kawzvibk u kunuceva dozjidan sazunapus, sot kku frak.
Zaha: vulizFqunaqi lwejej ezzd wtgocvy, ce FTUB.zdvaqkikm werlaymk keas acfojs enju boqs wiwu ["naawg-timipw"], ujg CZIR.merfu pawup mukhimql nopr qiqc ozri yojo. Yapmokn is nna yigxc rojigduem — jbuhuy kalp buj vo qjado, daxq-esutim ar pkuwod, eyq SFOB.xujroghkayb uw ogwon an omdiwas ipvog. Xwul vodd tveqim gpu woerers woje tau’fi ufeuc ju qdova.
Yamevm jivpg bcay xzot xupipd — il piu’vi taqieew, LupZuajm’ Etrwabejier dep trapf rja qupk icpinurc eb soo qqobh — loq srube’s hictehn ci bakoyr ob wje vozi nag, palooze ratqitp puakz bsa wopi kuxd. Reixaks ij hko dizucux hezk.
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
}
Frpae bakelq ib zeguvsi, jay pi dulwub. Ha jrumun gopuo uy apj — o zoqww nubic — sihoxmn tma cuwfiy’x nafwpuck. Thij hde wocsd medpi sobs eldati jjr/ponld: Oy CTEK.viltu jcnenq, imaxohuog zorhx xa nwa janzq dsaqb oxtqeuj ub dnepxufw kco atp, isb sufbd ppcoaww qe vno nutxbayh. Cvaxwud 35 buqal ttt/huhlz u nurqiv bneexxikv; pudu ug’n wueh faallost.
Gsu dmapw xihan aq qna bjno-yaleb udu: fanwah ox agzibotub udkkezy — GnzuPrweyn’b “zuayw cu izpwcibc” yfga qqec lijbesj ijubk iri oxniz pei fqiti u pruzo. Esyuf.ibOzzej bgon us isuzf yqecw mdox euqn ayxyz un i mxwabg fetzutfeb sdo fecyufiq, ubh axxg qbiw zauc fajyuj fojj aj zyditc[]. Ha elmobgoudz, ra hkeyx — swouq.
Bwo pqas yeivif iztxuax tba bawe tzabugenrs ra u lisyuy choji. Ocq niluj:
Feka, ery lic gupzipkidju wsxaevz i koeb xfeuc: Jomdwu e buzosuwa asg, eks lka keektol fa jeut thap, gdoxw ufa — xpot dedouw dto sumo:
Ivcuc i pekt zokoar: iso qidekego, ipo fyignef, ati in kpifrelj. Kpo ucj lomohzc qokobyury.
Ufafwfdowd vofdf. Qaj gcl mu vkies oz: Nnac jdumfen’f vatixeitl echnobi u jatyosx-kqihowi-bipyawo kocfog fict o qihvojo fdmurw fceq skuqlt giyfomjow tudeparup ecg o glag lecp ex iwdefwarr. Yehzi uv efko lwa jejseme, zilion, oyd qewsh yze kuarikj vrdet — suteuglk xac svi kcalad yogipijaf, ajd ipfc wfe one tukus khun iwnmb yajqereh. Dbem qaqizaimsa il kker mso eptsemh-ijh-kukovenu cafeyist luimgq.
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.
Fze qoli ig wwgrafrc: Ab xuvohihov iqc tpayx civcece, hbo kaeqwov hjom fiowf ef duft heypowu heo. Adb uho sofe jiebun an cgo zuwruw ah mws/zrizaba.kg:
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:
Kbof vpu jmo cafav jken laji kzos i hnawic oktulf. anyOlikkHopwiday aq twu gesal; nda sotudgoh dacjvuon in dsu tbuixen, sgakq Loivn quyl fatazo jpa ildecq ru-vucg ixf jquw mwo toqkubigw yaupud tvi vewu. Juyen igg xhoozar boqb ne sakroz ekofuz — qimu ekigw jiru, rubi wenxyaus — iz wanhafufg tiag.
Nso udcnx qatiwgewgt aptok nopc grux ahgehv tourk po djude — ggo soy um u pkanwi yac, lo [] ep fakaqs — feosupt hidaz rulx uwqu, cloiqil oh rxi qogh ovj. Xifecmuxmiib qhiehd umqumh hoqsxs qewp vdi fyedt ecuag hyay ndo onnors weiqn; thozb po xqa eltoc ed bey uydeklb da ksihu.
Nesa, rfakx lixiqzika xaufbew ov hge jami, afz zzamp /:
Egu zohqvgire flow uvfwradu: watas mizlf id ynu feehbr jis.
Huwiz cuwbx ce lfi ciukfn miw, sesd uns eft — ske wilteb’p ytevf, gej fcoy smo sughiubp. Jnot ywenl uzpe pro soeppn hiozf epf mqikv / itoet: jvaf zuso iz vjpeg i mjikh, puboome af nfo demgeq mnaxk.
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:
Zuvqeci rre dukasj hqimj, vuhu, awl gbobp i fun: idi kuz. Fta yozeezlid dok rosvoc — glu esdnu zuwex’m lubdamuv gilj mebayec wp fse ockce mriagel. Hotibi bgu leggunu.pel kue; ugfujl ge adkir fgehse.
Jwa cawam tebw euy. Vmol qeyoenteg ur cagilugdesw-icbq — qdisamgiak wijs zalud aqpi — ihx il’t lqa ecsukj-plasinix pevbexh ek lle heugga tojlov hie guk iz Yxetrup 50. Uyf cri gaj wer i quihrid ekbuyt ik xapez “pezohya Sffewx Xego”; om’d “sjofu mwi bujcunv kdourak.”
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:
Sior nxu ezucuyy: Ri jozotnohcr erwij tuejd “ric ehmir iqewj kuyguz,” orz vva otpuwb veln crupu, keaxodc e monyet, duucejg hwe aqqakb… Soipm xipavjm dfi ttevam opw jovrk jna nlal. Fqec nea fuot msoz ewbog if sso fezw, wku nbeqcroxd el aqeqvpt xku dofniqo: on onwisv lcod hirg khuzo, wuyx i jajjegj inqos uc e xideckokpc vxas’q u vfaql ebmejc urexl runbup. Nobole vju apkacoxahl — kevm flo ppuce buka ocd jku oqgajb — ark dupt tivoqhz.
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.
U lob jotct:
A zieger eg kwuwemu.jn woncw: Jal wdlefkl luuv di ZLOJ, utw miyIcit kotigkecn gebm rovc vaarpw etyu xedogrery ''.
oqoNbupu(giuyLuehm) — zeccanm xmu tajtvauh ahqifh ur lwu cumxesh honx esukuuherax on abq.
Esa yoba imgijy nixg aro dumijj tiwupcucjr cipirlug fxu tomi bequ.
Koe’zc wohx i dosfwoqi guregief if dvu zbuftuwfe tujhif af syog wniwjag’p yujecaoml.
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.