HomebaseWorks
Lesson 3 of 4

Controlled forms, validation and submit handling

Bind inputs to state with one change handler, validate before submit, and avoid the warnings that reveal form mistakes.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

Almost every React coding screen asks you to build a form: a login box, a search bar, a sign-up page. The interviewer is checking whether you understand controlled inputs, whether you stop the browser's default submit, and whether your validation logic is clear. This lesson gives you a pattern you can type from memory.

Advertisement

One state object, one change handler

A controlled input gets its value from state and reports changes through onChange. With several fields, keep them in one object and use the input's name attribute to decide which key to update.

import { useState } from "react";

function SignupForm() {
  const [form, setForm] = useState({ email: "", password: "" });
  const [errors, setErrors] = useState({});

  function handleChange(e) {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  }

  function handleSubmit(e) {
    e.preventDefault();
    const next = {};
    if (!form.email.includes("@")) next.email = "Enter a valid email.";
    if (form.password.length < 8) next.password = "Use at least 8 characters.";
    setErrors(next);
    if (Object.keys(next).length === 0) {
      console.log("Submitting", form);
    }
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email"
             value={form.email} onChange={handleChange} />
      {errors.email && <p role="alert">{errors.email}</p>}

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password"
             value={form.password} onChange={handleChange} />
      {errors.password && <p role="alert">{errors.password}</p>}

      <button type="submit">Create account</button>
    </form>
  );
}

The updater form setForm((prev) => ...) matters when two changes land close together. Spreading prev keeps the other fields; [name] writes only the one that changed.

Checkboxes, selects and textareas

A checkbox reports e.target.checked, not value, so give it its own handler or branch on e.target.type. A <select> takes value on the select element itself, not a selected attribute on an option. A <textarea> also uses value rather than children. Here is a second worked example for a checkbox inside the same handler:

function handleChange(e) {
  const { name, type, value, checked } = e.target;
  setForm((prev) => ({ ...prev, [name]: type === "checkbox" ? checked : value }));
}

Common mistakes

  • Forgetting e.preventDefault(), so the page reloads and state is lost.
  • Passing value without onChange. React warns and the field becomes read-only.
  • Starting state as undefined and later setting a string. React warns about switching from uncontrolled to controlled. Initialise with "".
  • Using for and class in JSX instead of htmlFor and className.
  • Validating on every keystroke from the first character, which shows errors before the user has finished typing. Validate on blur or submit.

Practice habit

Build the form above from a blank file without looking, then add one field type you have not used (a select, a radio group, a number input). Doing this three times in a week makes the pattern automatic, which is exactly what a timed screen rewards.

Advertisement
Continue with React4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Web and app