AniUI Academy

Controlled Forms

Making React state the single source of truth for an input's value, the controlled/uncontrolled distinction, handling many fields at once, and the case uncontrolled is required.

9 min read

Forms are where "the UI reflects the data" gets tested the most, because the browser itself also wants to own an input's value. React's answer is the controlled input pattern: state is the single source of truth, and the DOM element just reflects it.

The controlled pattern

import { useState } from "react";
 
function NameField() {
  const [name, setName] = useState("");
 
  return (
    <input
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  );
}

Every keystroke fires onChange, which updates state; the new state causes a re-render; the input's value prop reflects that new state right back. It looks circular, but it's the same one-way data flow as everything else in React — the DOM never holds a value React doesn't know about, which means you can validate, transform, or react to every keystroke from one place: state.

function NameField() {
  const [name, setName] = useState("");
 
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value.toUpperCase())} />
      <p>{name.length}/20 characters</p>
    </>
  );
}

The trap: value without onChange

If you set value but never wire up onChange, the field displays the state correctly but never updates it — from the user's perspective, it's frozen, no matter how much they type. React logs a console warning for exactly this ("provided a value prop without an onChange handler"), because it's almost always a mistake rather than an intentional read-only field (for a genuinely read-only field, use the readOnly attribute instead, which suppresses the warning and documents the intent).

Multiple fields, one handler

Repeating a useState and a handler for every field gets old fast. A common pattern uses one state object and reads which field changed from the event itself:

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

[name]: value is a computed property name — the same JavaScript feature from the objects lesson in the JavaScript track — and it's what lets one function handle any number of named fields without hardcoding which one.

Uncontrolled inputs: the other option

An uncontrolled input just lets the DOM manage its own value; you read it only when you need it, typically via a ref (covered properly in a later lesson) or by reading FormData on submit:

function QuickForm() {
  function handleSubmit(event) {
    event.preventDefault();
    const data = new FormData(event.target);
    console.log(data.get("email"));
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

defaultValue (instead of value) sets the initial value without taking over ownership of it afterward — the DOM tracks changes on its own from there. Uncontrolled inputs are simpler and can be a fine choice for a form you only read once, on submit, with no live validation or per-keystroke behavior needed.

The one case that's genuinely uncontrolled by necessity

A file input can't be controlled — browsers deliberately don't allow script to set value on <input type="file">, since a page silently "choosing" a file the user never selected would be a serious security problem. File inputs are always read via a ref, never via value/onChange.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A controlled input's value comes from state, and onChange writes user input back into that same state — React state is the single source of truth.
  • value without onChange produces a frozen-looking field and a console warning; use readOnly if that's actually intentional.
  • One state object plus a computed property name ([event.target.name]) handles many fields with a single handler.
  • Uncontrolled inputs (defaultValue, read via ref or FormData) are a valid, simpler choice when you don't need live, per-keystroke behavior.
  • File inputs are uncontrolled by necessity — browsers don't allow scripts to set their value.

Check yourself

4 questions · pass 3/4 to unlock Lifting State Up

up to 50
  1. 1.What makes an input 'controlled' in React?

  2. 2.What happens if you set value on an input without an onChange handler?

  3. 3.What's a clean way to handle many form fields with a single onChange handler?

  4. 4.Which kind of input is essentially forced to stay uncontrolled?

4 left to answer