Reactintermediate 9 min
Build forms with several controlled fields, a submit handler and simple validation.
How to manage a form with several fields using one state object, handle submit, show validation errors, and reset the form.
Sign-up, checkout and search all rely on forms. Controlled forms keep React as the single source of truth, which makes validation and conditional UI straightforward.
You already know a controlled input: its value comes from state and onChange updates it. For forms with many fields, keep them in one object:
name matching its key in the object. [e.target.name] is a computed property, so one handler serves every field.onSubmit on the <form> element and call e.preventDefault() to stop the browser reloading the page.checked and e.target.checked instead of value.form holds every field. update copies the old object and replaces one key using the input's name.type === "checkbox" selects checked instead of value.validate builds an errors object. Each failed rule adds a message under that field name.submit stops the reload, validates, and only if there are no errors stores the data as submitted.errors.name, errors.email, errors.agree.reset puts everything back to its starting values.Press Join with an empty form to see the errors, fill the fields in and press it again. Add a rule that the name must be at least 3 letters.
e.preventDefault(), so the page reloads.name and state keys, so a field updates the wrong value.value for checkboxes instead of checked.update handler can serve them all.preventDefault, and reset by restoring the initial values.Next: routing between pages.
const [form, setForm] = useState({ name: "", email: "", plan: "free" });
function update(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}function App() {
const [form, setForm] = useState({ name: "", email: "", plan: "free", agree: false });
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(null);
function update(e) {
const { name, value, type, checked } = e.target;
setForm({ ...form, [name]: type === "checkbox" ? checked : value });
}
function validate() {
const next = {};
if (form.name.trim() === "") next.name = "Name is required";
if (!form.email.includes("@")) next.email = "Enter a valid email";
if (!form.agree) next.agree = "You must agree";
return next;
}
function submit(e) {
e.preventDefault();
const found = validate();
setErrors(found);
if (Object.keys(found).length === 0) setSubmitted(form);
}
function reset() {
setForm({ name: "", email: "", plan: "free", agree: false });
setErrors({});
setSubmitted(null);
}
const err = { color: "#b91c1c", fontSize: 13 };
return (
<form onSubmit={submit} style={{ fontFamily: "system-ui" }}>
<p>
<input name="name" value={form.name} onChange={update} placeholder="Name" />
<span style={err}> {errors.name}</span>
</p>
<p>
<input name="email" value={form.email} onChange={update} placeholder="Email" />
<span style={err}> {errors.email}</span>
</p>
<p>
<select name="plan" value={form.plan} onChange={update}>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
</p>
<p>
<label>
<input type="checkbox" name="agree" checked={form.agree} onChange={update} /> I agree
</label>
<span style={err}> {errors.agree}</span>
</p>
<button type="submit">Join</button>
<button type="button" onClick={reset}>Reset</button>
{submitted ? <p>Welcome {submitted.name} ({submitted.email}), plan: {submitted.plan}</p> : null}
</form>
);
}