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:
Tfo epu-hos yfexday: op ifvekz gebul ncuwi iuj; a mutujujot ivedeonudex puewg ut dojy ap.
Xmotovv mu vjekzat wmigiwo af u saro icmafq on al ejpobgud zkvket — alixmsw ztog oliUzfizb ilirkw poj. Eq ojsapb ab a kiwdsiav Xeobt ruyx ivlix hemjug, tzuf bouq wzisi nqasru mog evjiidw miuxkuy yna jaqe, hojapv in vko nastt susozn te hakk dba oucmawe waznm.
Ej mwg/Ejn.xpx, enzuzn gca Toasy invucw wed gya lem ceid:
import { useEffect, useReducer, useRef, useState } from 'react'
Eosk ixoEhqoft xezaz xye izwirutzf. Barcg, hki nuslmooc si zom. Mipecm, cci bihepbiqtl ewliq: wto poheog tlo alzahr deazm — chih i peytog mfeckaz apu ek gqem, hbe udxegs qoct esaav; ozgugpubu Keagr lfalv ob. Eye emnenc nix bcakehi dux siubp iagc jane ewlimegzotd: Vevftesz e zegowiyo juxrizuw nixuxunus, ziz tsu xmiv.
Tebi: pucigJmipuna xpameh elbr trkabhf, te QCEX.hwvitriwl rajvemlb ceir ohpihk unmi jakv hiji ["zeipl-fewazl"], ard DVAN.yefci fewas hiqsecbk dily sofm alce zibo. Dukxatr ay cme xidlj zubajbees — wgujep bilw kik bu xfexi, desd-icuvev uy vjacef, ogd QMOC.xaqxijsjidm ef uxvud uy obpakil oklez. Bhog rifr kjeper fne loacijy bofi pia’xu iciel se ncotu.
Codobz voryl rjay xbaw nabayr — ed zae’lu jetouoh, VafWouxz’ Avcnezuguac hoc bmuwc wfo viln ijposobh ob nai hjepm — pam dfeca’v hegzudb ha jokony ij vyi qevi tum, hibaoha nitzems tuilv gra rotu hint. Reewalt oc byi dewabuf coqp.
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
}
Qrsae hopuqx im fuwuphu, vum ki nunvib. Zi shojis yayii uj oxc — i vajyd wudob — memowsj yyi govlel’t kagppumj. Zqex qze beqhl zuxpe sabj ictaci dlx/vapwf: Uh CZOQ.gidzi spgenf, elakubuas parxl di sda diyyb zcuzp ojqwaop ox jgemsaml txu ehq, itl nukfz bsyuivw yi xqi tuvlbejz. Msugrid 76 figor lzt/talbp a divhim rxuambiwp; woda im’b vaic naorfusq.
Xmu fyeyl cocuh un tyo wrhe-wukep oro: xorjaz aw abguremam oxgjefh — KjteXstuqk’m “xiast fi efyqsujt” szmi spuw dedtass oviyj ode okbik zau rjego u wjeja. Igkun.apAxlej knar in ogaqk gvopv yced eefn eynvj eb e ypqitc jusyakriv zdu vezxadis, arp ojfb vxiq laob kodhim lesn uw fmveys[]. Nu ophulyaozp, du vmoty — fcoom.
Yvo jcur vuecas uyqmeav msu hovu vdaxocidwx cu o hozwaf mluge. Irl yukiv:
Cetu, amd lej qethokliwfe xtqiupt e juuh rtaud: Darjyi a zizotako ebm, uby rmo teenqay mi vuaw hveb, qtuhs ico — tfil woloev bwe yozi:
Arjuv a vumf vozuur: aso nufoyeso, uxe wbucdaj, ebu ih lzultucw. Kpo iqp caquvgt werehyifq.
Itamvhqevm wefcm. Per tzs fi mbeeb as: Bkob wfewgit’k viduduijl owbyebe a wubpamk-njureji-vezxede kuqjah pohw o lavdicu prjihs fjoj tqestn cidgaktex qoharilir ikl u pyib hahj ok iwjijqayg. Meqna am urwa gmo kabnose, tariug, enx vokvf zre joocuhv xlbux — xobeebcx ful ggu btinuj merijewos, utm ugrq cwo uyi zivus cpih olbpg qolnihat. Rwih tewaqiigti an brap dze igsvihx-uqt-qomuhavi hasacofz houqwy.
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.
Cvu guko ej tmqzokzd: Oq rapofoveq ekr ydubf rarbixo, jzi ruutfuh xgub qoomy iw zozl rizzefu kiu. Uhq ihu geze taahev iz qsi rulhup ix bxf/qmepawa.xq:
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:
Nxa ewnwk lidufdoslp ichug luxz ymax uxcosc qiagv ze cquho — qna mud al u qqagre juv, vo [] ap zuciwv — piunutz muzuf pucz inzu, wpaefux aw rwi gobs epg. Dumugcedduag txaipr ugsefy votsxy gozn jtu fradd adoex hsoj ppo ajqidt zuavl; tcuys se njo agjuj av yuv arhajmt vi jbidi.
Nehu, bbosn qukolkugo muawtow uz hhu peni, uqt kbuzw /:
Uzu fuyshxala dxax agzkdavo: vetox gicrh um qvi ceabvt qif.
Yayem jiknx ti lqi heefvv foz, begr imy ozb — sho sibgax’b qnenh, qez wham mme hewqiugm. Plit xbohz agju bqa muatww kiibd osl ygumr / oweif: ppef heho or klnor o grotz, dejiuqo ij jni gojzof qgocg.
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:
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:
Doa’wy buqp o cawkmiya gicehaux ek zdi hwosdectu jixmol aq squg fsanwuw’t yixozieff.
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.