Forms

Course 1 · Ch 7
Forms and Controlled Inputs
Making state the single source of truth for a form's fields, instead of letting the DOM hold it

Challenge 1 from Chapter 4 already built one controlled input — an input whose displayed value comes from state, updated through onChange. This chapter generalizes that pattern across a whole form with several fields, plus the input types that don't use plain text (checkbox, radio, select).

Controlled vs Uncontrolled

// uncontrolled — the DOM itself holds the current value <input type="text" /> // controlled — React state holds the current value <input type="text" value={name} onChange={(e) => setName(e.target.value)} />

An input with no value prop is uncontrolled — the browser's own DOM tracks what's typed, and React only finds out by reading it later. A controlled input ties its value directly to a piece of state, with onChange updating that state on every keystroke — React becomes the single source of truth for what's in the field, not the DOM. Controlled inputs are the standard approach in React, since the current value is always available in state wherever it's needed (validation, submitting, displaying it elsewhere on the page) without ever having to reach into the DOM to ask.

One Handler for Several Fields

function SignupForm() { const [formData, setFormData] = useState({ name: "", email: "" }); function handleChange(e) { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); } return ( <form> <input name="name" value={formData.name} onChange={handleChange} /> <input name="email" value={formData.email} onChange={handleChange} /> </form> ); }

Rather than one useState call per field, a form with several fields commonly uses a single state object, with one shared handleChange reading the field's own name attribute off e.target to know which key to update. [name]: value is a computed property name — plain JavaScript, not anything React-specific — letting one line update whichever key matches the input that fired the event. The ...prev spread keeps every other field's value unchanged, updating only the one key that changed.

Forgetting ...prev loses the other fields
setFormData({ [name]: value }) (without spreading prev first) would replace the entire state object with just the one changed field — every other field's value would be wiped out on the very next keystroke. Always spread the previous state first when updating just one key of an object, exactly as covered for state objects generally back in Chapter 3.

Checkboxes and the checked Prop

function NewsletterSignup() { const [subscribed, setSubscribed] = useState(false); return ( <label> <input type="checkbox" checked={subscribed} onChange={(e) => setSubscribed(e.target.checked)} /> Subscribe to newsletter </label> ); }

A checkbox doesn't use value to control its display state — it uses checked, paired with e.target.checked (a boolean) rather than e.target.value. Wrapping the input in a <label> lets clicking the surrounding text also toggle the checkbox, a free accessibility win that costs nothing extra to add.

Select Dropdowns

function CountryPicker() { const [country, setCountry] = useState("ie"); return ( <select value={country} onChange={(e) => setCountry(e.target.value)}> <option value="ie">Ireland</option> <option value="hu">Hungary</option> </select> ); }

A <select> works the same as a text input — value and onChange on the <select> element itself (not the individual <option>s) — which is a small but genuine difference from plain HTML, where the selected attribute is normally set on the chosen <option> directly.

Putting it together with onSubmit
The form's actual onSubmit handler (from Chapter 4) reads from the same state used to control the inputs — there's no need to separately collect values at submit time, since every keystroke has already kept state in sync. handleSubmit just calls event.preventDefault() and then does whatever needs to happen with the already-current formData.
ElementControlled via
<input type="text">value + e.target.value
<input type="checkbox">checked + e.target.checked
<select>value on the select itself, same as text inputs
<textarea>value + e.target.value, same as text inputs

Coding Challenges

Challenge 1

Build a form with two text inputs (firstName, lastName) sharing one formData state object and one handleChange function, using the name/computed-property-name pattern. Display "Hello, [firstName] [lastName]" live below the form.

📄 View solution
Challenge 2

Build a component with a checkbox controlled by boolean state, and a select dropdown with at least 3 options controlled by string state. Display both current values below the form.

📄 View solution
Challenge 3

Build a feedback form with a textarea (controlled) and a submit button. On submit, prevent the default behavior, log the message, and reset the textarea back to an empty string.

📄 View solution

Chapter 7 Quick Reference

  • Controlled inputvalue tied to state, onChange updates that state
  • One shared handleChange + name attribute + computed property name ([name]: value) handles several fields at once
  • Always spread the previous state (...prev) when updating just one key of a form-data object
  • Checkboxes use checked/e.target.checked, not value
  • Select and textarea are controlled the same way as text inputs
  • A submit handler can read directly from state — no separate step needed to "collect" form values
  • Next chapter: useEffect and side effects — running code in response to renders, not just events