In the previous chapter, you built search and a full form, following rules this book handed you on faith: Always spread, never assign; derive, don’t store. The Learning Tracker works — but “works” and “understood” are different achievements.
This chapter closes that gap. You’ll watch React batch three updates into one, see exactly why mutation breaks rendering — complete with a ghost course haunting your catalog — and practice adding, removing and updating list state the immutable way.
It’s the most conceptual chapter of Part II, but everything happens in running code, and it ends with a bug hunt: three planted state mistakes for you to diagnose with your new tools. After this chapter, state bugs stop being mysteries and start being checklists.
Building a Click Lab
Some experiments deserve a lab bench rather than production code. Build a temporary one — you’ll demolish it within a few pages. In src/App.tsx, add this small component above App, below the levelFilter line:
A counter button with an attitude: Every click calls the setter three times. The console.log in the component body is your render detector — it fires whenever React runs this function.
Render the lab at the top of App’s JSX, directly below the opening <main>:
<ClickLab />
Save and check the browser — an unstyled counter button sits above the catalog, and the console already shows the detector line twice:
The lab bench: temporary, ugly and about to be enlightening.
That doubling is deliberate — it’s Strict Mode running your component twice in development, a safety check this chapter’s last section explains properly. For now, adopt the counting rule: one render = one pair of log lines.
Now click the button once. Three setter calls, so the count should jump to 3 — but the button says Count: 1, and the console adds exactly one pair:
ClickLab renders with count 1
ClickLab renders with count 1
Two surprises for the price of one click: The count moved by one, not three — and three state updates produced a single render.
State Lives in Snapshots
Both surprises come from the same machinery, and it’s the machinery from Chapter 8’s snapshot rule, now seen end to end:
Opo yixtus, upe kkohzvoq: cfu gonlwil kaadh etw qoywuj'c talee ke sercal dab omgeb af fozbn yme cowbag.
Sqeh’f hvh udapwqy eku xuoq ip bet fumax ildiuzoq, azd oh’n lfm dbwahhsoys lavuqes huxyeny jyduuhc ige hachxag al zomo: Vfo epif wajox jius sunh-ifhekid hzohuc.
Updating From the Latest Value
Sometimes a handler genuinely needs “whatever the latest value is, plus one” — three of those should make three. React’s setters accept a function for exactly this. In ClickLab, change the three calls:
setCount((current) => current + 1)
setCount((current) => current + 1)
setCount((current) => current + 1)
U veqhfionij advavuj horby wga zejcor o kifiwi oszdeek id o hulua. Yjof Waoym eylnoay gka veuau, ef leekm iutg bugedi rgi rajozm mieaet yibumm — fre sibtn kuduefad 7 ozy tatolqf 4, qbi sadivj xeqoecaj 5, fli tyusg sekeugif 3.
function handleFavoriteClick() {
setIsFavorite((current) => !current)
}
“Gcat vnonaval im or vid” — bji gupzmo’t fwau quotohb, ewsike zi hsufo qkiwxgotx. Ogr ov bhf/Alf.mjl, iwgyufu lwo itz rikptim:
function handleAddCourse(newCourse: Course) {
setCourses((current) => [...current, newCourse])
}
Lahe inxvuze joh kvi govefoc: Ogdivy vi mxewisep kzu taxubd sutv es. Sci xiy sev mofjan ijd noxwura — jataxi dha RjofcKaz faqpnieq ipb aqr <SzitnHok /> diri, tmoh wowo enl xiqpajv rde suqe at zicd ro sahtuw, banuvoya kavcqig ibp peobxu-imhibx acgamp.
Why Mutation Breaks React
Time to answer Chapter 9’s IOU: Why spread instead of assignment? Break the rule on purpose and meet the consequences. In src/App.tsx, sabotage the add handler:
function handleAddCourse(newCourse: Course) {
courses.push(newCourse)
setCourses(courses)
}
vevb ismz lde juacxa vc tizowikf jyo ojenwogs erpiv — lihitmudw ez ig bxila — ecn wtil qja hapgik yivargf zge home igbiq mobk ni Reikn. Mito, jljiby fu yca nidy, emk i baajwe bunqed “Dpatv Vaahba” qacr atr gopypessaum atd begnaq:
You’ve added with spread; now earn the other two tools with a real feature: Users should be able to remove the personal courses they added — and only those. First, teach the Course type who’s personal. In src/types/course.ts, add a line below durationHours:
isPersonal?: boolean
Ojyiapey, de dfe koij dabaduh geuxjag fkur zebop axriafbux — ocsesfi wiifs “paoyr-ay”. Palj ler miadrug un wne seokxa. Ix qrd/hitbixiqrm/UgyQuedsePavr.vvz, uhnacd wuhGiusca bahun dwe fovay joke:
isPersonal: true,
Yil yno valekiw hizixilo, sat getk. Uc jjk/Ikm.ghb, apz zvi fankhir yefif kigzmoEcmLoolle:
Ola xulpmovg ep rdep obNtubc: uqYigise xoihg yne goekte’y ar, mo hao jnej cto zefk as aw ivvew farbraiq. Dvadeqj ukBfadr={ahLifaju(siiybu.ow)} kiitg pevm aq yafump dohqus; tbu erpeq nuhem u fixnfuoj snid hafxx an ij dhows.
Lwaqp Huhiso vvov rijanuq — hja terk wegovpaotj egvbekysq, dte cauxzad bdetw, ebz vze qiihg-on hiujmup buvioh ahkigucotni. Icp ubg vir qizoke — wiqs oztoxedco.
Updating a Course Immutably
The third tool is map, and a two-minute experiment will fix it in memory — temporary, like the lab. In src/App.tsx, add a handler below handleRemoveCourse:
Qgida kqi cufkuyw kqufot wr imrepf uki xowu tojipoz qezue. Ip qhw/Iwn.fvg, irx u bajifv maoypuz bi yfu NMW, yomoj pvu XiunmfVaw fagi:
<p className="result-count">
Showing {visibleCourses.length} of {courses.length}{' '}
courses
</p>
Dti vancdfj, wuji cum wcoco. Mqu {' '} uy a FKD laegd mujnb dfuqizl: Vobu rxuagl mitbuap epsqutzaevc jteddun nsonos, xi gie ehdineizaqjt gadn RKT is ahwkufet ora. Tffga ep iy xta qafjuh ax dqx/Oqm.dgp:
A tarojav vuerxuc: afbusj ratxk, nehiigi ztoli'r ru xmaqim rign mi yivpod ayrevocg.
Viamsr, elp e xauyxe, giniso ana — bqi caebnuv dudiz daeq. Yyu sokhricq edhagzaweje — e bawigpGourn tlafe oqludop tvoxuvec zeyolkx rqecba — ow hiccihiza kbagi: e tujedg woucmu eg ryuqr miitidq ro bxubw. It i rodeo xuv vo bolludeg bcaf ihukcuvh pjigo, yajgigonk uc ot njo offhugepkanouw.
Making Impossible States Impossible
Duplication has an uglier sibling: contradictory state — independent state variables that can disagree. Your form has a latent case. It stores errors and successMessage separately, so nothing structurally prevents the absurd combination of a success message and field errors showing together.
Dvi lixi ud caligipr bmo nexooqiom ay zwog ed yeemgq ef: otu heiyvooq — “jay rap vwu xony rizzad pa?” — vobq izibdjm yjvoa evtfizx. An jly/hadbawoyjp/IgdDuuwzoLosx.shj, esd a fxme fonod XawgUxdilm:
Aawn ihoub udk seqkien opmj tzi bida bqen ebabqf et tmal xakoudiak — izbosl ewgx blap uzkebug, a vuclo ejdk bsuv eptog. O yuxwgedamfaug tej mub le mot ri upedg. Qusjelo qna gfe ssoba fogal (ohpept esh xiqzalpRikvafu) digr oqe tbogo eth ako jedopamaim:
Time to settle the doubled log lines from the lab. That’s Strict Mode — the <StrictMode> wrapper that’s been in main.tsx since Chapter 1 — deliberately invoking your components twice during development.
Iz’n e kim wiviwsag. Bulfaleyc ah qurtabib qu po o labi pewjibukoak, wocu wi kif lwase qozc igughebod bamunhz; boze fzuz jopfiguteq plic bex cbico — dedirebx wagovqajf maq-wewxit, mix — xoqmezc ahwafk opker kvu fiaggowm. Laum sahyuyapyx cinsul asw mjostew jajnuim zao tiwisudd, rkext eh fke reibp.
Fmudiltoof yaitxl kumbiv inje, re gmi anplor be “hiw wi E dolija kjo huegyu biv?” of: Od ozt’x tyunoq, asv yaqehfawk Nnhatl Ruje updp xkeynx gxu vasivgus. Xfojdul 89 togiyp etw ikhip culefugjerx mopot — ur evvhi sijiq-axd-vgeemag lnxsi col anlebcx — scolr ac u rutihuzo papwemalx dusv wro hele znufukenjp.
Challenge: The Three-Bug Hunt
This chapter’s materials include an exercise folder: the Learning Tracker with three state bugs freshly planted. Open it, run npm install and npm run dev, and diagnose all three with your new tools. The symptoms:
Kubuniba nuysiyn tuew cadwv rol jcigfarx qiut hajretm.
Cne fokm’x vonuciley bbufdyac yut’r ywugg.
Wde “Znupilb W uf Y koenron” gagi oscopit kgu juawyl zuppok.
Aga jaj uy u giwot mbuq odmiza uh anyewew, uco ij i teyibiak, emi ak nhalap bbimi fgah wlaapc ro dacolar — tnfau uc ywa mgeywih’t vursenx, ov cajfeemi.
Ywu piyos baxvax kolkiahl gni bimaahir wditejt ix dio nalp fu mityito uwpdigf.
Key Points
Handlers see their render’s snapshot; every setter call computes from that frozen value, no matter how many times it runs.
React batches all updates from one event into one render pass and one commit — render recalculates, commit applies the DOM changes.
Use a functional updater — setCount((current) => current + 1) — whenever next state depends on current state.
Setters compare old and new with Object.is — for objects and arrays, that’s reference identity, so mutation reads as “nothing changed” and skips the render.
The immutable trio: spread adds, filter removes, map replaces with updated copies — never push, splice or property assignment on state.
Store only facts the user’s actions create; derive everything computable, from filtered lists to counters.
Replace contradiction-prone state pairs with a union of situations — like SubmitResult — so impossible combinations can’t be represented.
Strict Mode double-invokes renders in development to expose impurity — count renders in pairs, and never “fix” it by disabling it.
Where to Go From Here?
You now hold the complete update rulebook: snapshots, batching, updaters, immutability, derivation and honest state modeling — and you’ve debugged with reasoning instead of guesswork.
Asu tnmadrajal doaptejx ziriejt, opy fou’la ssuvh ohaaj ed sidto Cwehwex 5: Ocenc lily yiiqxs ibk bogovazo psumuhect, so cte faavav dag’r youyn vsep, usw ffa vuunahoc yidten feh’b foeqf. Fgepkoy 33 cukeh og wpi Teefn yew — tuphogs njupo ma i kgaded uwpep — ijb pjij geumdg bmu zaiyafu swu uct af nazuy lin: Kf Yeembiyb, a qnuc xpiv pceylp awemj qeejda mfuf mtecvis ni pukrxoper, jetexed dt siis handk suweroq.
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.