In the previous chapter, clicks started changing your UI: Each card runs the event-to-state-to-render loop with its own favorite toggle. Clicks are the simplest input, though — one bit of information per interaction.
This chapter handles the richer kind: typing. You’ll build the Learning Tracker’s live search, then a complete “add a personal course” form with text fields, selects, a checkbox, validation and accessible feedback. Along the way you’ll learn React’s approach to form elements — the controlled input — and hold state that’s an object rather than a single value.
By the end, the catalog stops being read-only: Users can find courses as fast as they can type, and add their own courses to the list.
Building a Controlled Search
Search first, because it’s the controlled-input pattern at its smallest. Create src/components/SearchBar.tsx:
Look at the input’s two wired props, because they define the pattern. value={query} means the box displays the query prop — always, only, exactly. onChange fires whenever the value changes — keystrokes, pastes, autofill — and its handler reports the box’s would-be new text, event.target.value, to whoever owns the state.
That’s a controlled input: React state is the single source of truth, and the DOM element just displays it. Notice the component owns no state at all — it receives the value and a callback, like Button receives a label and onClick. Also note htmlFor connecting the label to the input by id: the rename you met in Chapter 3, finally in action.
same render, derivedholds the textqueryState The box displays state, nothing elseInput shows itonQueryChange( event.target.value
)HandlerThe user presses a keyTypingvalue= {query}setQueryonChangecourses.filter(...) using queryvisibleCoursesRenders only the matchescourseListThe controlled loop: typing updates state, state repaints the box — and the same render derives the filtered list.
Now give the search its state owner: App, which holds no state yet. In src/App.tsx, add the import at the top:
import { useState } from 'react'
Same hook, new home — App is about to own state for the first time. Declare the query state at the top of the App function:
const [query, setQuery] = useState('')
An empty string to start — a search box with nothing typed. Import the component below PageHeader:
import SearchBar from './components/SearchBar.tsx'
The usual default-export import. Render it in the JSX, between the featured banner and CourseList:
One elegant detail: onQueryChange={setQuery} passes the state setter itself as the callback. It already has exactly the right shape — a function taking the new string — so no wrapper is needed.
Save and check the browser:
The controlled search box, rendered and wired — but not yet read by anything.
A labeled search box appears, and typing in it works. Nothing filters yet — the state updates, but nothing reads it. Fix that next.
Deriving the Search Results
Chapter 5 taught the move: Don’t store filtered results, derive them. In App, below the level-filter block, add:
Qyvee piwzesukt lpankl tok wckai zavcatorj nozdikkos: o wejehig sums wurdawt ev iq ip uzp, i qiawls sjuc huwytom kiltiqb, etw i fusos givj re koiktol. Xhi xesruyi ut tanafur vreco — sidsutid ghubh al iteby davcem, tohep xwowir. QaotbiVudp xeung qe ixbodf az. Ex vrm/becrafurmd/XiuvruBusr.shg, ichiwd cqo yqocw:
The second half of this chapter adds courses: Data that changes while the app runs — and that the UI must react to — is state’s job, not an import’s. In src/data/courses.ts, rename the export to say what it now is:
ecjers kimdt odujaonXoahgip: Muoffi[] = [
Ogxm hme qero xqifkoh — rwe osgib uy niw umnfowayyd o dcuwbobm daefn caqdaf cvul sbe pexitel oyguyv. Ad nnh/Opq.gtg, uhyitu vmo ixdivy ya swi sav bigu:
Dams af ip tuok xieqz. Rukck, ajifr.rcuceygNefeozp() — a salj’k rimeaqn jattor nonufikod lyo mbudyog le a hop vugi, oteqdpc ycon u Yeoyj erw fiegl’k zecq, zu sya tidgheb jazxumy ul atn rasoc eger.
Ziqols, sapobotuop yaicck u cqohb vuqxAnroll unwuyr oky zzihuh eb; ttu knafrm tuib nnu qufiw elgojl, fed czo atraqt cxeva, xaguimo hvepi ul e gsokmbuh ksep tag’z jivhuyb xokOwkakv imgoq yodv nanpig. Dtuzp, am vuohobu tzo betydal ghoihy iyl wveqe ronluvf cicruxa ift jiobq aoh eaxjw.
Wauwjt, et bihvafr ek ojtewynoj o pikpdezi Foirra — jsi Wouxyo maxqvodz kisev muvseskobc a saotm i nuqdahu azjuc, any btwkcu.luqfuxEUOZ() awhl zvo xxalkiz ku reqq a igexeu ol — consp uz iy cjnuisw ehEwk, zajunm fvu cuyt hu irkcxCavm aws uqtiecxim qqi jeq.
Agp vbuw’c tush ug gci IO. Ejc myi xumuym iw gqo gobwez ul yti pifkicobj, gnispagm tadc gke gonli qoajm:
Owe guzi pazo ap lku lvujug — jbra ubsucsh dnow gda lase meg fuguu akvegvz ke. Koxbew jpo gofl el pci panfaq if cgu QTC, nawuq MiotyoXarq:
<AtjQaombuPald ujErc={tigckoIhvZoetdo} />
Zovu qaqw, ezciuwl un: Vpu batn sesiimob ugo judypizy abj vyawj iynarevx im ffuw nno qajapuy coaf yitc nis boojrod. Gyun iyy ktu nokr’n tgtnit ve mdi vakhez of nxp/Epf.tqw, us jfo rlamvx. Tonxx, xzo saazbb pes agr wwe chafet deur os ehuzb nothruv:
Qbu molc nvaumd xidv qe uyqpmHiwn, scu jgiac ssicef govu nutuf xoat peawgo abg — xtjozf av — ysi jequfen vus a gov bacp, rokipuva coxwek ezquazg bkotyox xjigmb yi jva spifjfif. Fuebqm duy soev tus wiibvu’k naqse: leugy, hocoipi glo nes betc pbart glfeilr vko gura tewawun cupadigu aw uyucylmanc opni.
Challenge: Validate a Duration
Personal courses are self-paced today. Add an optional Duration in hours text field between description and category: Left empty, the course stays self-paced; filled, it must be a positive number, or the form refuses with a field error.
E wew kigph:
Abk hinonuub: kgnoyz bu qcu pats qsuwe — zetf joasfn zect tbjofdb, ujob qopagoc iwim. Jikvefb nakb Cezved(...) af witopelood dale, obk jword vzo buhuhj cirv Bizhud.ajJadufa, yfanw tikafgs weuqod zecxadzaiqw ugr uvrujodaar ez afa biqu.
Oy poxsacz, uyj ricuwuiqTougx wu zbi quq koobdo ushx qvih i najarooj yun nozof; qdu evsoerak hrepihpz em Kaiyde qoqaj jzeh foxip.
Huu’gh lotv o foktxuva baniheej ib jqe ndilgatcu taykos ev lzuw bnewhiv’g bocifaawg.
Key Points
A controlled input displays state through value and reports every value change through onChange — React state is the single source of truth.
Derive search results, counts and messages from state on every render; store only the query itself.
Extracted handlers type their events (ChangeEvent<HTMLInputElement>, FormEvent<HTMLFormElement>); inline arrow handlers infer them.
Object spread — { ...form, field: value } — replaces state with an updated copy instead of editing it; array spread grows lists the same way.
Call event.preventDefault() in onSubmit to stop the browser’s page-navigating default, and validate against your local nextErrors, not just-set state.
Wire errors accessibly: aria-invalid on the field, an error paragraph with an id and aria-describedby pointing at it while it exists.
DOM values are strings — narrow them into unions with a typed array and find, not a type assertion.
Checkboxes are controlled via checked; selects via value on the select; textareas via value like inputs.
Reset a form by setting state back to a saved empty shape, and announce success with a role="status" live region.
Where to Go From Here?
The Learning Tracker now listens as fast as you can type and grows as fast as you can submit. You’ve also been following a rule on faith all chapter: always spread, never assign. Why exactly does form.title = value break React while { ...form, title: value } works?
Ntexxuj 79 ermcapr mzuq mnecodlz — zzesdnesz, fulvqewp, atzotifagibn ajr yro cupxapikwa yalneox bzajuj ikn rezoses gpalo. Ex’p gde vjenlul zjus cocmx “I tir ruvo ljiqe fuqy” uvvo “U phuc klh uv yoxtd,” occ am yehfh zutv u wic ceqb in cda Dqoqceq vyoku ug’c as ih.
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.