import { useState } from "react";
function LoginForm() {
const [email, setEmail] = useState("");
function handleSubmit(event) {
event.preventDefault();
console.log(email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}The input's value is always exactly whatever is in state โ the user can't type anything React doesn't know about, since every keystroke fires onChange, which updates state, which re-renders the input with the new value.
const [agreed, setAgreed] = useState(false);
const [plan, setPlan] = useState("basic");
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
<select value={plan} onChange={(e) => setPlan(e.target.value)}>
<option value="basic">Basic</option>
<option value="pro">Pro</option>
</select>Checkboxes use checked (not value) paired with event.target.checked. Selects work like text inputs but with event.target.value matching one of the <option> values.
const [form, setForm] = useState({ name: "", email: "" });
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />Using one handleChange function for every field, keyed off each input's name attribute, avoids writing a separate handler per field โ a standard pattern once a form grows past two or three inputs.
An input with value={email} but no onChange becomes permanently read-only โ React logs a console warning, and the user literally cannot type anything, since state never updates in response to their keystrokes. Every controlled input needs both value and onChange together.
Build a controlled form with name and email text inputs, plus a checkbox for "Subscribe to newsletter," logging all the values on submit.
import { useState } from "react";
function SignupForm() {
const [form, setForm] = useState({ name: "", email: "", subscribe: false });
function handleChange(e) {
const { name, value, type, checked } = e.target;
setForm(prev => ({ ...prev, [name]: type === "checkbox" ? checked : value }));
}
function handleSubmit(e) {
e.preventDefault();
console.log(form);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input type="checkbox" name="subscribe" checked={form.subscribe} onChange={handleChange} />
<button type="submit">Sign Up</button>
</form>
);
}
Q What's the difference between a controlled and uncontrolled input?
A controlled input's value lives in React state and updates through onChange. An uncontrolled input manages its own value internally in the DOM, and you'd read it only when needed using a ref (covered in Chapter 20: useRef).
Q Why use event.target.name to handle multiple inputs?
It lets one handleChange function update the correct field in a single state object dynamically, using [name]: value computed-property syntax, instead of writing a near-identical handler for every single input on the form.