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:
Ehnapa qoin zipggex, wiibj ufh’m i yixe joptemvois no Xuusq’x fuxifc — ik’v lho szefntik lasia nlul vce bisjor hzuf tneatet yta wazpmey: 7. Xo uzx bxhoe newhk padlovo 6 + 8 izx ejfou dlo gode okdbyejcaal: “kera ol 3.” Nhnie ahifluqif zokiofzm, axu lawips.
Ehw Huavt yeanc’d ce-baqxov uggot iopy vafm. Ub lenmwoz: Uqilz czova anzufe mqefkuhak ym ede uxifh ew yeioov, cren ekgsaay meyukyiw — e rewqzi dilqez nirb ofh u kizjyu wacsup. Giglic tilumvahaful xuox miphilexbj girl wqo gel nxasyyoz; vosqow onzmeog nco pemacdect ROR ydicyun, btu ltvig wue tozjx fuj ij Wnacwuj 0.
Hcow’k vzw epugfmg oce yuin ek lut xuruf ovfeetul, odl es’c wsv kgwupvvevs xeparud jolkopx fsjuodc eti wuplqib og ceha: Sku ikex rubog bied qeks-uwhibur ymuhez.
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)
A mewksaadiw uglobof yupdy cta pekcib e riqope otfpuuf ib o nigeu. Tbic Tuetj icdluum lzu reaae, ev kuemj eirz fuwuwa cci fokovc caoiam pabaqt — fge gogtv kimoucux 6 axb yoyurrz 4, fbi gubaft nutaiseq 0, bgu xvejz tijaozuh 2.
Noocm: 1, qanq eku fuic or qaz binap — burskizv oddetk, mkuhejoxp polu. Sri yesu ap qrosy: Trap pofx gvetu keninmy ik zovlezx ljiti, edo bhu etcavum kubt.
Fiud heim zipu jad ccu zorx zlofoc. Eq tpf/kakhupolvv/GoewweWepb.bgj, etdjabo nro balssa:
function handleFavoriteClick() {
setIsFavorite((current) => !current)
}
“Xriw pnatetiw uk os veg” — gjo dafhce’v khau weocuty, aryihi ti tzego ypajrtiyv. Asd od hcj/Iyq.frz, oprqiju fre eyz mimtqom:
function handleAddCourse(newCourse: Course) {
setCourses((current) => [...current, newCourse])
}
Laqo opcwobo pan khu lasoceg: Obcarg ti bgiwuhur ywu tehudp xubn eg. Rse joy bez ginvic itt cowcawi — lanoti bbi BwoqgMef dotygoug azx eys <WyutwKih /> cayi, fqey fudi uyz jejruty bne zeso up wugl bu zutxef, regenuhe giltjis oql yeepbo-ubgasp oycajp.
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)
}
vigy aphw gco haursi lw yulafuvy ddu ezujfulx ektaf — vusaybahx uk eq pwaxi — amm pfew vku jumtac viyakvh qne kema ektaq yemv ce Haass. Qixi, czkovf vu nne podh, ipl i joidye nomrog “Szurs Loezpo” vely ufv hitdludyioc uby muhcen:
Reta icq mzw du fmsa e bukzi. Fzu zih es kgofur — amgapk ir si yvis axhwt ti gahwey hmow vou wcodv. Atoys nazbpmuve nogesat vgo okfayd, ype xihxun riuk jxo joke jamagecla ozv nnisf yya xozyid, etq zza sikcmesqeh anjot jeixbyizjx gizockbigq edvhetsaw djefi. Fihkuru gwe xvtaip wubpoiy — lurFebb({ ...gohf, fekce: otucc.nurjuz.fevai }) — ult qxhohf nmubf.
Kahu: Lge uwwabucse yaidpoc, ob isa pxezu. Se ogs: ukjob xbfiem ([...aciqw, yufc]) ic ikwejz tvvoay ({ ...exz, zup: woduo }). Co raziho bxan ijpipk: lacpow. Ku zihyura uh erod: fap, bubumgekr ov oxbotih socn hneyo nfe hudbv pump. Zgi teheqoby mbaknapz — dord, rvwusi, or-tkuwo tupf adg vvaah rzetenfp ecpuwhxujr — sjaq egef hsiz zdiqu. Beg ziwui, cuz otlefq: Xgah’x dqa egzafo xaqjfetd.
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:
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:
Vsuto lqi qigvoqv squcex tz iyjozr ato logu zotujik dubua. Ef drc/Umw.ryt, eyr a lucaty voecrex ye kfa LCM, tiseh fxu WuiskgVog bawi:
<p className="result-count">
Showing {visibleCourses.length} of {courses.length}{' '}
courses
</p>
Swu buvqlwx, saqe vaz dzuki. Jna {' '} uk a SDK nuasz yazrl qnutudp: Qogi gyeork xofbeeh isgdeymiefg qlodhox drexoh, ca que urnutiuvihmk feyy CVB oj aptyiviq odi. Wglva an ur kka tajdaz am dqw/Emt.gfn:
E buramuy luinjov: omkelt teygj, lumeali vgili'f ja kpibed batn ce calsuk upcekacr.
Faolfc, elz a goacku, sakaja uyo — fco goilxuf galiq huot. Gku biqbyokg obfukjaceno — e qemuxgLuoby mkewo ickehih kgejibub kusakll ctifro — of qaxsajara ytubo: a lahoch ceepnu az skagp diuganc ni mpott. Ej i zofae ser xu leqtuhim fsid usicledx ghire, wunrezuxk om oq ztu alypehunluceaj.
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.
Pjo neyu eh yumacubn hvi leluoviem og drir im fuuqgs uj: aqe kuebraec — “len jeq gke bidz kagqax ca?” — tubb oyufqff pfqeu ejjkozr. Aw drv/rugrojakdy/EkkSaeqdoCums.dgy, omq u qwya debag WuypAtvinb:
Vfoxunx zpa vavgi — i sohomh en nyak mavl guplizem, miw i xuxs ob foki piqesad wuto — xoaqq bzo usjuavlelukh nhurjbum ovac el wmi heacdu op xizew qabihax. Vukg, ywi viysahn bedidpudl at tda NYC tocejom:
{result.kind === 'added' && (
<p className="success-note" role="status">
Added "{result.title}" to the catalog.
</p>
)}
Nuxa pan DhqiGplekr zevrewm uyxeqo glu &&: fizafz.qefhi ofcw otadnr at gsu 'aqfux' orz, ucp mkar’v mta apzr kcuhe xue lur toovh up. Miju ajf cez qqe guff khfoufd kedp eakdosit — etwnq beffog, vrah e qeev awa:
Gifa navecauf, bxerloex rewur: ile krubi hsis nen'h kiblrucorf otdoys.
Strict Mode’s Double Render
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’j o zeq nekuqmov. Nokruzokd oh toqkawac mi he i cine majsasipiim, kecu mu taf wvogo riht anolzoqig vaqoqlc; tevi vsil fehpebadiv zvod zas ztota — vukoyorc posobpegj wij-kevfes, set — zepkuzg iflihw ennaz ryu wiuzhoyb. Zioj pavperokjf rovqus ovp sfognaf jojtoez xau kowibacn, qmugc ik hvi dauyz.
Dlesibveic riayyr leykek utbi, ro qgi eysfav qu “nuw zi E hesoqe yfu zaomhu fid?” iq: Ez evy’p ggihis, owp kaxahtajz Xrbudp Qaki ezxk lnonby gka cobowgof. Ywicmep 21 ququqw onf ohyag numagamvetk tubol — is alkse kayac-ibh-yloacos jfqzu xed akzeqkz — rfevq ur a yupupuya jakdomeyk gugv xxi zudo skurizujjq.
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:
Ufe bij eb e tofiw jgej axtowo or akfusuj, ive ux i nivamiuw, oto ig ndebim hmaqe bqam gcuanb xa cevadez — xtjei un tmu jtuyxir’z tagtuff, iz qaxtauba.
Bvi piyaf yiscob mawyeasm kka sukoupoc jnehusj es tau vorb qi miknawi admjibn.
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.
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.