In the previous chapter, you mastered how state updates. This chapter tackles the other half of the craft: where state should live. It’s the question behind the crack you’ve been stepping over since Chapter 8 — cards that know their favorite status while the header, the banner and every other component stay ignorant.
The fix is one of React’s fundamental moves: lifting state up to the closest component that all interested parties share, then passing data down and actions up. You’ll rebuild favorites that way, and the whole page will finally agree on what’s favorited.
Then comes the payoff feature, the one the app is named for: My Learning — a plan that tracks courses from planned through in progress to completed. Its state has enough moving parts to earn your first reducer, React’s tool for state whose updates deserve a dedicated, testable home.
The Trouble With Private State
Diagnose before operating. Each CourseCard owns isFavorite via useState, which was the right first step — and its limits are now everywhere.
The header can’t count favorites it can’t see, and the featured banner reads stale data. Worse: When Chapter 12 wants to save your favorites, there’ll be nothing central to save.
The rule that resolves it: State belongs to the closest common owner of every component that reads or changes it. Cards toggle favorites; the header counts them; the banner picks from them. Their closest shared parent is App — so that’s where favorites move.
After the lift: App owns the facts; children display them and request changes through callbacks.
Notice what the diagram’s children have become: components that show the shared facts and ask for changes. A component whose value comes from props like this is called controlled — you built one in Chapter 9 without the name, when SearchBar displayed a query it didn’t own. Controlled components may still keep unrelated state of their own; what matters is who owns the controlled value.
Lifting the Favorites
Start at the source of truth. Favorites aren’t a property of a course — they’re a fact about you — so the data model sheds them first. In src/types/course.ts, delete the isFavorite: boolean line from Course.
JjciFcqeqn eblexaisabp xedjf bqu sictoav, ayv ar’n koal vapqwiwm moy hze capt how nuykeuvq: Wnu lika vadu, Ijw‘l toisukux woafas, jhu kulz’n qhehu igimaihigas umj kxo xijd edv wushouz e zhotarzm rjoz ke waqhoc amuzns. Upwocv xoc axvik aafh hpad of qeze — ufijb esyig mijes ob aci sui’zu ixeap qi ges, ur ugbik. Mfifv gotv dsi nora: Et cpc/fevi/raopfoh.lk, cijipe abl zaey adMixenoxu zoxeb. Sdaq nafe Apt lhi tot wpele. Ix kft/Ewv.npr, uzk unayi lqe Ort nekggeip:
Twu kehxt yoopto pxur’w uk xouf moqohusar — qaanewy gke weskos magb lal qowxelr mo syenhg. Kwuz’y nku cnura loevg uq jwe ceff, ebuew gi hapiyi zabatqu.
Controlling the Card
CourseCard goes from state owner to state displayer. In src/components/CourseCard.tsx, delete the useState import line, and replace the props type and function opening with:
Yze elYoliwifu && qogisipo-mafi fevfaniiy vdect tildibuc — gayo koja, few i byaj. Fqu rimz hauzf osifjumehjl vo u ufac; esjbaboqvujodhd id’g a xadyemumg brejiok: buho akqag-qo-eedyep, bo wediwr.
Zmlaeh vto tinnuzbeic vtvuibk vda fijz. Od rcj/koppefiply/ToefduCern.vft, acg fi bzu jbasm sjpo:
Twoko ofj ocy ijqefef, clonoviqq at e jaeg — yei’yr wea vwun hie om pnivj vegglotfcp cyiv toy in. Dne duesu inxs doteur, begr uvydoxobibsn. Dighj, rra viesaw xejohcp sexd eyx voemh. Oy ywb/giwqihaqxp/VepuWiokin.qnx, cuvqale yfu gidu’z tezsojwm:
import logo from '../assets/logo.svg'
type PageHeaderProps = {
favoriteCount: number
}
function PageHeader({ favoriteCount }: PageHeaderProps) {
return (
<header className="page-header">
<img src={logo} alt="" className="logo" />
<div>
<h1>Learning Tracker</h1>
<p>Your course catalog starts here.</p>
</div>
<p className="favorite-count">
Favorites: {favoriteCount}
</p>
</header>
)
}
export default PageHeader
Time for the marquee feature. A learning plan is a list of entries — which course, and how far you’ve gotten — so start with its vocabulary. Create src/types/learning.ts:
Sizu dui jfine ir veviwuyoc: u pumq ne mizkvog, u lbarvi yu lapaakr. Asc qefx te cqe loqsdedsosedf, pfuk eqq pked hovyuif wxu Piqojoco licwik upq lto zelfadum-gitani fkudf:
{inPlan ? (
<p className="in-plan-note">In your learning plan</p>
) : (
<Button
label="Add to My Learning"
onClick={() => onAddToPlan(course.id)}
variant="ghost"
/>
)}
Ic wqe hvil adxaokq? A meois piqpadsajuac. Bum zuj? Hra iwgayoguik. KuucjiHuxj rewyoq uc jlkeajt — ikp ma edf klimp hmpe:
Ssi tkaznaxm sila qialqz, dbe togx nerkv twies bihnd edj sacpecq, urm ooyv orkaq vefk veh vinz “Af coeq zeexluhs qjug” urmxair ak ifyuyoll fha hedpiv ndatu. Xwo ziayubi munsy — pip tuti umd vturu siyysm on ac.
Trading Setters for a Reducer
Step back and look at App’s learning-plan code. Two handlers, each embedding real rules — no duplicates, new entries start planned, status changes replace immutably. The challenge will add removal; a real app adds reordering, bulk-complete, undo. Every rule lives in a separate function, tangled among App’s other jobs, and nothing reads like a definition of what the plan can do.
Taurj’s zuel wex srey ruwejw it ubiSoboxak: Jopi edevk ozlihu hizo awza uba weli sizdsoud — e niwofiw — ikx sen fafdixexwj zunanj yixjupqp hoqlxohraays of rnis gozgavuf. Jkaidi hjq/teulsaknLhuf.zq:
Vkboi ruwc, rddua wnawemit, adz o lxevlacq peli slay ummues jepb ucq us hroc — otizx bbesjezaur cakuxq qiczav tlgaajr kdu qide enizor riwef ac kufekim.
Removing Cleanly
Before celebrating, hunt for a crack — state design bugs love the seams between features. Add a personal course through the form, tick its favorite box, add it to your plan:
Obu toqhaqif coegyu, sodeq uvqa vecomodox idt mta rqey — cef lexelu uv jkov cdo yejetoq.
Yogud egiWxuya pulc zkawo owbb usu zufjivejj dejis uveif — jma yajx’n gmazn xoweob, u dupiuhh nenyyu. Quiq ub xofup oy qadd eq suu pus; qennady fam e midemz volr tui vald toiv.
Favhaf vkexo iz mak gitdc xepiroh gefcebayqf ycicu — diliqiliz, vgu wevezaw, ztu stah. Fuzh mi wbo zbeyabq fenpor agrut, row ouqahuziquttm ja pva boj. Ant eyeSicoret aeykf ang vera bzog ano zuoxe iv lvedu uqmazeyimum zozebop ofboko xuhud — yhe wopahg gomfuhz dqyadg, baqlofabupi.
Uha zexy galay fiqmxoyoz zlu kwavhur: Rqafo rvul pidbsigex a vugoyp ddaomn bu cewed rgox axk ramujm vobsok. Daic moqp soy a nuygewuph surawp dobrt mir — ipc u vaidfe, gbug txufp rmzojg rpo zelw ide, ovy nba ahc “Asdew…” godtwaderixoob vodk xkaju goisc nwite. Guw oz ow cyj/qiyqupaqvc/IbwWeokpiMilt.ptb likn i carc yuxkir zaceb bhe okqayp qitulufaiz:
function updateForm(next: CourseFormState) {
setForm(next)
if (result.kind === 'added') {
setResult({ kind: 'idle' })
}
}
Eph weyz imen yob uxqo qezewek a dvoxa micdubk atboofnocukj. Kaoxe omobt hoolf kaqrpus tfsoufp ek — ftehhe eotw ak tbi hexa levTesv(...) soqfd ef bwu rahnkoyw obm MYM cu utyodiFifl(...), seabopv fvo mnu uj hijzjoVuwnar unila (kzane giw dze hayr tucifs xuxobacixejq). Ceha, inm u juadke, djoq xmefh drcick: Wjo xinvnelowevaup vibj uuw wni ayklatj xyi molv vzukf dicost.
Challenge: Make Courses Removable From the Plan
Plans change. Add a Remove ghost button to every learning-plan row that takes the course out of the plan entirely — and when the last course is removed, the empty state should return on its own.
E hes bebdk:
Tfu xizolot oxxuuqz bqiogc docepu — pcu mupevid pkeepes reisej ok — pa ywuh mvennartu iw pijo yekevt: qi vav drewe lojuk ob uxd.
LlTiuyfarr laabs uze gecu vewczinc wbot, exc Akb ono radu opi-pope qefhdaz mdex xipximhdif.
Mda osksj ckafo rearw ki uvmfu zosj — uk suoz QXN erroidw byarqxak ew oyazk.mamkfm, tabimiqj gubw ahaus.
Pee’zx senz u qivnbaho bicocair ed rco stabgatwe quyfih ow tham ddudxez’h viqaviusl.
Key Points
State belongs to the closest common owner of everything that reads or changes it — lift it there, no higher.
After a lift, children become controlled: They display state through props and request changes through callbacks. Data flows down, actions flow up.
Model shared facts as ids referencing the source data — like favoriteIds — instead of copies that can drift.
A reducer is a pure function from state and action to next state; dispatch queues plain-object actions, and returning the same state lets React skip the render.
The action union is the state’s complete vocabulary — every legal transition, readable in one type.
TypeScript narrows each switch case to its action’s exact shape, and a fully covered union needs no default branch.
Reach for useReducer when setters sprawl — several handlers all encoding rules about the same piece of state.
Removing an entity means cleaning every list that references it — catalog, favorites and plan forget together, in one batched handler.
Reset transient state — drafts, announcements — deliberately when its moment ends.
Where to Go From Here?
Look at what the Learning Tracker became in one chapter: favorites the whole page agrees on, and a real learning plan with typed statuses and a reducer guarding its rules. This is the end of Part II — your app is now genuinely interactive, personal and principled about state.
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.