Skip to main content
    Courses/React/Forms and Controlled Components

    Lesson 5 • React

    Forms and Controlled Components ⚛️

    By the end of this lesson you'll be able to wire any input to React state, manage a whole form with one state object, handle submit without a page reload, and validate fields with clear error messages — the skills behind every login, signup, and checkout screen.

    What You'll Learn in This Lesson

    • Build controlled inputs that bind value + onChange to state
    • Store a single field with useState — and many fields in one object
    • Update one field of a state object without losing the others
    • Handle submit and stop the page reload with preventDefault
    • Wire up text, checkbox, select, and radio inputs correctly
    • Write a validation function that returns clear error messages

    1️⃣ Controlled Inputs: value + onChange

    A controlled input gets its displayed text from React state, and reports every keystroke back to state. Two props make this happen: value={name} tells the input what to show, and onChange runs on every keystroke so you can call setName. Miss either one and the input either won't update or won't be controlled. Here's the smallest complete example — read it first.

    function NameForm() {
      // useState holds the input's value. '' is the starting value.
      const [name, setName] = useState('');
    
      return (
        <div>
          <label htmlFor="name">Name:</label>
          <input
            id="name"
            type="text"
            value={name}                                 // React DRIVES the input
            onChange={(e) => setName(e.target.value)}    // every keystroke updates state
            placeholder="Enter your name"
          />
          {/* {name || 'stranger'} re-renders live as you type */}
          <p>Hello, {name || 'stranger'}!</p>
        </div>
      );
    }

    It's a loop on every keystroke: type → onChange → setState → re-render → the new value appears. State is never "behind" — it is what the box shows. The runnable box below walks through that round-trip one key at a time. Run it and watch state catch up.

    Worked example: the controlled-input loop

    Run it and watch state update one keystroke at a time.

    Try it Yourself »
    JavaScript
    // The controlled-input loop, simulated step by step.
    // In a real component each "render" is React redrawing the input.
    
    let state = "";                           // pretend this is useState('')
    function setState(next) { state = next; } // pretend this is setName(...)
    
    // A user types "Hi" one key at a time:
    for (const key of ["H", "i"]) {
      // 1) onChange fires with the would-be new value
      const eventValue = state + key;
      // 2) you call setState -> React stores the new value
      setState(event
    ...

    2️⃣ One State Object for Many Fields

    For a single box, one useState('') per field is fine. But a login form has an email and a password, and a separate useState for each gets noisy fast. The cleaner pattern is one object{ email: '', password: '' } — updated with a computed key. A computed key is the [name] in { [name]: value }: the square brackets mean "use the value of name as the key", so one handler can update whichever field changed.

    function SignUp() {
      // ONE object holds every field. Keys match each input's "name".
      const [form, setForm] = useState({ email: '', password: '' });
    
      // One handler for ALL inputs. [name] is a COMPUTED key.
      function handleChange(e) {
        const { name, value } = e.target;              // which input changed
        setForm(prev => ({ ...prev, [name]: value }));  // copy, then override one
      }
    
      return (
        <form>
          <input name="email"    value={form.email}    onChange={handleChange} />
          <input name="password" value={form.password} onChange={handleChange} />
        </form>
      );
    }

    The golden rule: never mutate the old object. Always spread the previous values (...prev) first, then override the one field that changed. Skip the spread and you wipe out every other field — run this to see exactly how that happens.

    Worked example: why you must spread ...prev

    Compare updating a field with and without the spread.

    Try it Yourself »
    JavaScript
    // Updating ONE field of a form object — with vs without spreading.
    const form = { name: "Alice", email: "alice@dev.com", age: "30" };
    
    // ❌ WRONG: returning just the changed field replaces the WHOLE object.
    const broken = { email: "new@dev.com" };
    console.log("Without spread:", broken);
    // -> name and age are GONE.
    
    // ✅ RIGHT: spread the old fields first, THEN override the one that changed.
    const fixed = { ...form, email: "new@dev.com" };
    console.log("With spread:   ", fixed);
    
    // ✅ Expected o
    ...

    Now you try the core move — updating one field by name while keeping the rest. Fill in the blank, then run it.

    🎯 Your turn: update one field by name

    Spread the old object, then override a single field.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — managing MANY fields with ONE state object.
    // A real form rarely has one input. Instead of a useState per field,
    // you keep an object and update it by field name.
    
    let form = { name: "Alice", email: "old@mail.com", age: "30" };
    
    // handleChange takes the field's name and its new value, and returns
    // a NEW object: a copy of the old one with that one field replaced.
    function handleChange(field, value) {
      // 👉 spread the old fields first, THEN override [field] with value
      re
    ...

    3️⃣ Text, Checkbox, Select, and Radio

    Most inputs bind to value, but a checkbox is the exception: it has no text, so you bind checked (a boolean) and read e.target.checked, not e.target.value. A <select> puts value on the select element itself — not on each <option> like raw HTML. Radio buttons share one name, and each one's checked is a comparison against the current state.

    // Each input type binds to state a little differently.
    
    // TEXT / EMAIL / NUMBER / PASSWORD -> use  value  + e.target.value
    <input type="text"  value={name}  onChange={e => setName(e.target.value)} />
    <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
    
    // CHECKBOX -> use  checked  + e.target.CHECKED (a true/false, not text!)
    <input type="checkbox" checked={agreed}
           onChange={e => setAgreed(e.target.checked)} /> I agree
    
    // SELECT -> value goes on the <select>, NOT on each <option>
    <select value={plan} onChange={e => setPlan(e.target.value)}>
      <option value="free">Free</option>
      <option value="pro">Pro</option>
    </select>
    
    // RADIO -> several inputs share one "name"; checked is a comparison
    <input type="radio" name="size" value="s"
           checked={size === 's'} onChange={e => setSize(e.target.value)} /> Small
    <input type="radio" name="size" value="l"
           checked={size === 'l'} onChange={e => setSize(e.target.value)} /> Large

    4️⃣ Handling Submit (preventDefault)

    By default, submitting an HTML form reloads the page and throws away your React state — almost never what you want. Put onSubmit on the <form> (not on the button), and the very first line of your handler should be e.preventDefault(). After that, the form data is just an object in state — validate it, send it to an API, whatever you need.

    function ContactForm() {
      const [form, setForm] = useState({ name: '', message: '' });
    
      function handleSubmit(e) {
        e.preventDefault();            // STOP the browser reloading the page
        console.log('Sending:', form); // now do your work: validate, fetch, etc.
      }
    
      // onSubmit lives on the <form>; the submit button triggers it.
      return (
        <form onSubmit={handleSubmit}>
          <input value={form.name}
                 onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
          <button type="submit">Send</button>
        </form>
      );
    }

    Here's the rest of that handler in plain JS: after preventDefault, you validate, and only send the data if there are no errors. Run it to see the full submit → validate → send flow.

    Worked example: the submit → validate flow

    See what runs after preventDefault on a valid form.

    Try it Yourself »
    JavaScript
    // What your submit handler does, in plain JS (no UI needed).
    
    const form = { email: "sam@dev.com", password: "supersecret" };
    
    function validate(form) {
      const errors = {};
      if (!form.email.includes("@")) errors.email = "Invalid email";
      if (form.password.length < 8)  errors.password = "Too short";
      return errors;                 // {} when everything passes
    }
    
    function handleSubmit(form) {
      // In React the real first line is  e.preventDefault();
      const errors = validate(form);
      const is
    ...

    5️⃣ Basic Validation

    Validation is just a function that inspects your form object and returns an errors object — one key per broken rule, with a human-readable message. An empty {} means everything passed, which you check with Object.keys(errors).length === 0. Keep this logic in a plain function so it's easy to test and reuse. Your turn: add the password rule.

    🎯 Your turn: finish the validation rules

    Add an error message when the password is too short.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — write the validation rules.
    // validate() returns an "errors" object: a key for each BAD field.
    // An empty {} means the form is valid.
    
    function validate(form) {
      const errors = {};
    
      // Rule 1: name must not be blank. .trim() removes spaces.
      if (!form.name.trim()) {
        errors.name = "Name is required";
      }
    
      // Rule 2: password must be at least 8 characters.
      // 👉 add an error to errors.password when it's too short
      if (form.password.length < 8) {
        ___ = "Password
    ...

    Common Errors (and the fix)

    • "You provided a `value` prop without an `onChange` handler" — your input has value but no onChange, so React makes it read-only. Add onChange, or use readOnly if that's truly what you want.
    • Checkbox never ticks: you wrote value={agreed} and read e.target.value. A checkbox uses checked={agreed} and e.target.checked (a boolean).
    • Other fields vanish when you type: you returned { [name]: value } without spreading. Always { ...prev, [name]: value } so the untouched fields survive.
    • Page flashes / reloads on submit: you forgot e.preventDefault() as the first line of your submit handler.
    • "A component is changing an uncontrolled input to be controlled": your initial state was undefined. Start fields at '' (or false for checkboxes), never leave them unset.

    Pro Tips

    • 💡 Match each input's name to its state key so a single handleChange can serve every field via [e.target.name].
    • 💡 Validate on blur, then again on submit. Blur gives instant feedback; the submit check is your safety net.
    • 💡 Only show an error once a field is "touched" — nobody likes red text before they've typed a single letter.
    • 💡 For big forms, reach for React Hook Form. It cuts boilerplate and re-renders far fewer times than manual state.

    📋 Quick Reference

    InputReact pattern
    Text / Emailvalue={v} onChange={e => set(e.target.value)}
    Checkboxchecked={v} onChange={e => set(e.target.checked)}
    Select<select value={v} onChange={fn}>
    Radiochecked={v === 'x'} onChange={e => set(e.target.value)}
    Many fieldssetForm(p => ({ ...p, [name]: value }))
    Submit<form onSubmit={fn}>
    Stop reloade.preventDefault()
    Is it valid?Object.keys(errors).length === 0

    Frequently Asked Questions

    Q: What exactly is the difference between controlled and uncontrolled?

    A controlled input's value comes from React state (value={x}), so state is the single source of truth. An uncontrolled input keeps its value in the DOM, and you read it with a ref only when needed. Controlled is the default React style and the one you should learn first.

    Q: Why must I spread ...prev when updating one field?

    Setting state to { [name]: value } replaces the whole object, so every other field becomes undefined. Spreading copies the existing fields first, then your one change overrides just that key.

    Q: Why use e.target.checked for checkboxes?

    A checkbox represents a yes/no, not text. Its value attribute doesn't change when you tick it, but checked flips between true and false — that's the boolean you want in state.

    Q: Do I still need validation if I add HTML required?

    HTML attributes help, but they're easy to bypass and can't express rules like "passwords must match." Always validate in JavaScript too — and remember client-side checks are for UX; the server must validate again for security.

    Mini-Challenge: Signup Form Logic

    No blanks this time — just a brief and an outline. Build the state logic behind a signup form: a field-updater that keeps the other fields, and a validator for email, password, and the terms checkbox. Run it and check your output against the expected line in the comments.

    🎯 Mini-Challenge: build the signup state

    Write the field-updater and the validator yourself.

    Try it Yourself »
    JavaScript
    // 🎯 MINI-CHALLENGE: signup form STATE logic (no UI needed).
    // You're building the brain of a signup form, step by step.
    //
    // 1. Start with a form object: { email: '', password: '', terms: false }
    // 2. Write handleField(form, field, value) that returns a NEW form
    //    with ONE field changed   (hint: spread ...form, then [field]: value)
    // 3. Write validate(form) returning an errors object:
    //      - email needs an "@"            -> errors.email = "Invalid email"
    //      - password length < 
    ...

    🎉 Lesson Complete!

    • ✅ A controlled input binds value + onChange to state
    • ✅ One state object + computed key [name] scales to many fields
    • ✅ Always spread ...prev before overriding a field
    • ✅ Checkboxes use checked / e.target.checked, not value
    • ✅ Start submit handlers with e.preventDefault()
    • ✅ Validation returns an errors object; empty means valid
    • Next lesson: React Router — turn your forms into multi-page apps

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service