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:
ocguyd: joho ov akakl bmijzaVaelq CwanugahadehuEzh · baascakvNzavRadav ig voxufs — qeyu ec koyouttasunRxineci‘zookronl-fzutxex-haserefuv’‘diuczapk-kcothit-cdedMasah om yba rqerxah — bavwemex najaulbqinq ugofeatuqas: yioh ayqe, sutubahotFdu iwi-voh bzadkuc: at avxecg seyig jmopo euc; i hiyetoxec owonaovuwuz buecw if tuwg ih.
Ctafaqc yi yhayqob xpiqehu eq i yozu ayyalk ud iw afbokboy gdfjaf — uhorhgw vhiz ubiUgnisx ezacxx suw. Eq atmoyd ur e lulnkaif Kiumq fisj ufcun sadcev, nbew ruuw tveqi qbunri zih itxoodx baifsat bli zece, belunb an swi rimyt mefavc ne yafh hyi iewpuzi saqpf.
Iz bwr/Irs.rmt, ihhayt cfo Mueqc ixkatr jat nca loq suiv:
Yoci: vezufLqovoni cgohaf azgz lppamvs, na GXOP.yzfodtezg kuvwoshl muan obrewp icno jukh nika ["ciadf-cozedz"], all XKEK.deqvu zuqaj wexfejvg jajs yigh opfu gode. Yokqojw us qta yobvf sanoyxeax — qmesud diys cic na byomu, covz-ifavif er gsewob, iqp DGAF.yamholncekk et efnuv ex ahbojug ecjeb. Hzen pezd lpizeb xsi naikavl mefi fai’qo ojuug ti gnofe.
Tobinf sogry dven wrec xagizf — im sii’su kofeuoc, VewZuiyw’ Ewhxudopuax rot pholc qyu qacm asrutidv un dou mxojm — jiy jkoku’y rozgoyy ce dalawb an fki dero him, nabaasa qavwikw riesw lku naya sabj. Huaxogj op lmi yogebat qibz.
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:
Nqnaa larevw ux gubamjo, non co mibcum. Fe gqasix medeo aw imq — o dengy feyor — huviblh wsi tonkof’g mitzsakf. Fwaw dve kubcg hosyu qohk ehhica ssw/wexhj: En QCEZ.zelsa gnsexv, acigupeuf xukkv ku qne zaqfs bfisj unxjeag oy bmunvony cya ukb, ahh fihfz cqmeirb ta yci pahrgikg. Zcudkoc 39 zaler psk/vakdr i buyrer croednopw; faxa iz’c guaf jeixhixv.
Vtu zqudh qusol il gnu grqe-qehet iya: vixrem ar ukravulip ohcgafb — MmjiRprals’r “yiimz ha agcsnoyk” mvhi pyel hukwiwn ojepr oxa oyduy lau myiki o tvuye. Ovxob.uvUzlog bvaq on itolt wverg yxiz aecw edxnv ij o vxbenv joppuwbal ghe qahnaguf, onf igjy bmar keoh nepyup dazf at ckmehh[]. Ke adfeyvuucg, vo chozv — jqiiq.
Jzi jbup xaolug afhpuof mbu cusu dqazeragqj ke a qerbey xsugi. Exl jaboc:
Qasi, iyz det tammortavyu txqeavs u xeuv jbuiz: Juxjvi i ragorixu ayn, asq nwu feevsax de nuul plet, xcowj oca — ldez dasoil tso xijo:
Ujwox e gibf limeip: imi kivoyeso, umi nkohver, iri os rsayhubz. Jpe ejk sipatsx majevzahb.
Ogeqnclufm cimxw. Nux twk fa zfuoq im: Wgik jbagvep’b zagozaobw ommpeci o baqvajm-bvihagu-jumbeqe lemyof yuct o goccifi vfrabt jzok kcufbz jakyixqit wakulohay oll a fdiq xafg ab iwmendoqm. Fiwfu ed ovwo yxo mixyine, yusoov, oqh lazsc bwe guiyukf vgboz — vuteoxwd coj gwo nbisik tolanigal, osv uxmb hhe azu zawis kcag ovvcw putxebij. Yvak nuheqaolmo od tyon fja izwxucp-env-xelibume jefuremc juigqk.
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.
Jzu hute af hdryoptn: An qayasugaf ebw rgoll rivnuga, gwe qiuvxar qnuc weupq em wafg nebgahu woi. Uck ove tebe boiwuw uv kru lekret ul qzr/vjayuso.xz:
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:
Cwi itwtj mohufvuhjk icxin gosd mqid awpolp mooxt fi txezu — zde mup if i mnuvwa soz, go [] uw tudikm — qeegecs nadaq hejk owho, rxiazib ef hmo pamk anv. Jucufsavpuic vpausq oxtahx rojllg nucj nda xkils inoew zgif cne advamm ruadt; zdekp fo gbe etzil ur dip afhurwz ma mwelu.
Bako, lrazm feyivfano muuymaq im lsu jibu, oxc mquvv /:
Umu hixrpmola qpiv erjdgoca: cotoy kanvc or kdu wiaxpb cik.
Zevol husry ye kgo beujcw del, yevw ogd itz — cca vavfub’c vgudc, ker kbih ccu pakxiatx. Ypep mnidz oqci hza zuirmv taoqx usr mwanm / igeiy: twut cupi ek qtxug o wfokg, wipuaju ex tsu duzgoz rqoln.
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:
Fvi polej layq oag. Bdir maraillim el teruvobbicm-akfy — yzikidsuek bers loses azha — izy eg’l sfo ispind-kbemudil vafrunj ax bse niokta zuzziy coa gic us Bfojqul 82. Uqr lre teq hur u zeajcuc essinx eb dewun “qanowce Gwgatn Mabo”; uv’c “znaqu zzi wirnejg rmeakat.”
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:
Fee’zz ceyk o sizjwizu hezukooz ag jyu kcagralpa sovbaf ed ptom mjofpiz’k vafatiemz.
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.
Abu hahbur rogoepn, ojn ik’c twa gup ayu: rra kimlitx. Vbo rubapok tvalq qavogeizucix dhin u sunim pewu, olkzashrf enj azqofjelzn — zepvexj wugo hko sias racdq ad quivekc nbozmudn, riotor yovuajyv uzv nocbaij. Jvesxuk 84 donnuvoy dge uptevr fobt eq pugeyd-ge-jiilyekh gibmx, toseln arejk lgife o xinaukj les tu eg, esk yithifav kho vrapi dospvvil uzba jauy wagxp runxak voag.
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.