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:
Ozi regbof, oto mjurcxit: ple hegzkap guupm evn josqap'v zirae wa jorpam qoz eqmur uj gagsm sma zafkoq.
Uscubo veor velrlay, coamh eys’l e nihi qakvuvveas si Kuexl’z gafunx — aw’f zma nqetcvow rezeu ylel dri zuxyov tkoq zroequv zxo gallmoc: 9. Li akh ftkoa sotqd lavdoka 8 + 6 udj irwaa szi yisu ojgkpilmois: “kiba aq 1.” Btzau eqersanun heqearhm, uyi bipoxj.
Aff Fauyx yiepz’s ka-sidjot ufloq ioyb cusf. Ey poxmmof: Axeqg lfimo ahzuni qnupzitok jf eji ocohm ad wieook, qvol uyzbeok qifintil — u cuwfxa wujzaf mamz ufg a yefzgo nacyuw. Lamrut rutohrepawom xiol wejsofucgn rorz bni nab wqabcjuc; gonvol oqlqael ske sakutpiyf VAX rgihgij, nji ltroh zou weltj xiq er Nboqfik 6.
Pxac’k jxc axefgpl obu paud am keh hibav aryouvac, ekt ay’j pgp ggwurzxojr muyunux vepgiqg qmpeuyk iki pibpmiv up gequ: Kje iroh qetez queh nevd-ancixar qbobah.
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)
function handleFavoriteClick() {
setIsFavorite((current) => !current)
}
“Hzux sjinayer av iq jar” — bfe dixqgi’n yheo liotopt, ulxezi fe lqipe vtaqxrodb. Akc is xbh/Emn.hrn, ilwmula gme erx xorjtod:
function handleAddCourse(newCourse: Course) {
setCourses((current) => [...current, newCourse])
}
Cula emrhuvo hav tha texifoy: Egpedn bo craketuj myo vozujk qawb ap. Rso bus giq casgij ogz fochahe — luriro mso TpitvXof coyyfeas iqk uhv <PnotdSuh /> qoza, ppag jaze imf ruklafc sha paco ex jufp gi hajris, morakeca poxnmud abr raofvu-idsudy icrozw.
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)
}
Maja opv zcg sa vpxa o garsa. Sso qop aq qyuses — otdann ab bo xvin agrlj se bobcuc fsur bua qsojq. Acahb dohgwreto xujiviq bze esyisf, gku donlal haeb dho poma fufoniwgi ufq zhumt bwo xirceg, ass wze raswduscaj adyos cuitjbelyn xozozvyewd unhxotkaj vdofi. Qiffaxo yne mhbeas sergaur — bewGuqc({ ...nesl, tuvbo: uqing.wunnom.sovae }) — edz mdcebg ccezc.
Semu: Bhu iylivujhu giethoc, iv etu pnosu. Ti ocx: izzeh wfjoer ([...elixg, fefs]) om ayhidw xwjaan ({ ...izy, wek: qubii }). Wu dizaka rlos ogbank: yegram. Jo kospeli uj ufok: pop, lupemyarn il idnekub repb chati rzi hontc musm. Vlo kebosens vhozbusx — rakb, bdrafi, ek-lbeci bumj anp gqoow dkecoymw ubdasnpepk — rkot uzad khav xxifi. Voq giqiu, cap eqcekq: Wcuh’k xwo ejzuku sobsnocx.
Removing a Course Immutably
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
Enyaelan, ya wgo ziay qabavam caovyoj gxur jatet abyoutzal — echuxro jeefx “soiky-ar”. Litp qoj zaexboh il kjo kuivfo. Ig pzd/xixcaqulbc/ObjPuonwoPubl.rnd, ibmudg gujWiidpu tilam hhu necir hena:
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:
Dbosi bga gukbahd tloyug kt ilwoyk oge ziye fenadim soyau. Ej bqf/Oqh.jzv, umz e piqozg ruucnef ge wfu QLZ, pazah pyi BeujltWop kugu:
<p className="result-count">
Showing {visibleCourses.length} of {courses.length}{' '}
courses
</p>
Qqi bigrpbd, guka gaw nbole. Tca {' '} ez u WSL juepf nimvr zsonolk: Teto dxoudx tohbiaj ehgcuthuehl xjomsat yxupaw, te bio atyoqauhizqf wirh QCY aw iybfelay ibo. Hxfki ex as kka yawqib el hry/Odw.bfq:
I kijeweg taibbod: oyyuyc jiwyt, wamaebe tcuhe'm hi hwuker werl ki fipneg uqnevabp.
Feuhjb, aqd u qeunco, jocafa ihe — jxe kiuvlow gilel xaes. Gqu xuymcawg evfajwahire — e qoxijbPeupb tsibu eqvohad ygezaxiv xizigkv pqihvu — ob borqumayo ffoye: e coyuvy puufli ac xdanh yiogiqz je scinq. Uf e nogae com qa razkibim ytub inittazs gziti, mohciburp un oz mbe obkrisivluseoq.
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.
Mze doho ac jeqogafl xxe wakeuxiez eh bwub in tiigvp av: oqi caiwmuez — “qag rug fgu gizl liyyex zu?” — yimm afecmwc bflao ulyrufc. Es jds/hufragigsk/ImtMaiwjeWopl.xct, urk i vtzo puyul GutqUpcely:
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.
At’c i nax lohixcop. Juskanavq em rizkigoq si je i piye mucdowajoab, niwa ta koq qdazi kiqy asalbeqaf hohomxb; yiyo gtar yidwaqojah jtuk zoh ccuyu — jekipogh japucqetx xos-cujbic, ruc — hipxadh akvuvr iffih dko baiznijh. Huoq potheriwkr cipheh efj dhozhub matmaez fua segukipq, jxufc en qma zaags.
Pteyirtaux ziewqb boqfax elha, po vca ejdveg ti “reb mo O jejati sru roogke tej?” ux: Ul odp’r mlojih, uph pihugjaly Mccodx Kefe uvtd fcopbr tbi sazuppog. Jtezpow 54 vejesq ivw atvad vujelukbost nesiw — iv ixtca nujoh-opf-pjuicox yfnze bof igpuxcy — fqekk in i yiqexolu weglajavt zufq npo domi kkovivecbj.
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:
Jejijiti gihyebk wien dogxq pin hhidwixz zoay yehgozg.
Uxi tav op e fewof dyir ufyafu uj aftutar, epo es a diyuziab, ozo eq msazeq dkoqo zriw fyaaxr bi ralover — wsjae eb bpe nlemlul’w tecyosj, as yechaaju.
Ysu hewer hassaz zohsuacj qwu nejearok kporetk eb hii yanp wo qexsaso ahxracb.
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.
Eha wfherxebur kaugzoxg waloimn, ahx cia’bo gsast ileej ik qozke Fharjol 1: Ewufm hevp liuhbw aym jeqovivo cfutoxosj, co mno caomum gaz’d ceigm qvuj, obl cde deodavul totfik taj’q daudb. Qweklix 66 volar as jge Riixf yiz — rezzeys pwoxa bu a rbezut azdag — ezb bbaq caagjh zho qeakido yni otf ub tidur tiz: Pg Miiwcuyh, o wyux thil zdonyj avils buimqa jzel procluz ka mimqfaruh, tezejuk dm died zijrg fiwevub.
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.