React

React Forms — Controlled Inputs & Validation

Thirdy Gayares
14 min read

🎓 What You Will Learn

  • Controlled inputs: let React state be the single source of truth
  • Every input type: text, textarea, select, and checkbox the controlled way
  • Multi-field forms: one state object + one updater function
  • Submitting forms: onSubmit and preventDefault()
  • Validation: deriving error messages from state, not storing them separately
  • Uncontrolled inputs: when useRef is the simpler choice
1

Controlled vs. Uncontrolled Inputs

In plain HTML, an <input> keeps its own value internally — the browser manages it. In React, you usually want the component's state to be the source of truth instead. That's a controlled input: its value comes from state, and every keystroke updates that state through onChange.

The alternative — letting the DOM hold the value and reading it only when you need it (with useRef) — is an uncontrolled input. Both are valid; controlled is the default choice because it makes validation, conditional disabling, and syncing fields with each other trivial.

2

Your First Controlled Input

The pattern is always the same: a piece of state, a value prop bound to it, and an onChange handler that updates it.

controlled-basics.jsx
const [name, setName] = useState("");

<input
  value={name}                              // 1. state drives the DOM
  onChange={(e) => setName(e.target.value)}  // 2. DOM updates the state
/>

React state right now: (empty)

Why this loop matters: because React re-renders the input with whatever name is, you can transform, validate, or reject a keystroke before it ever reaches the screen — that's impossible with a plain uncontrolled input.
3

Every Input Type, the Controlled Way

Text inputs use value / onChange. Checkboxes use checked instead of value. Selects and textareas follow the same value / onChange pattern as text inputs.

input-types.jsx
// Text
<input value={text} onChange={(e) => setText(e.target.value)} />

// Checkbox — use "checked", not "value"
<input
  type="checkbox"
  checked={agreed}
  onChange={(e) => setAgreed(e.target.checked)}
/>

// Select
<select value={plan} onChange={(e) => setPlan(e.target.value)}>
  <option value="free">Free</option>
  <option value="pro">Pro</option>
</select>

// Textarea — same API as a text input
<textarea value={bio} onChange={(e) => setBio(e.target.value)} />
4

Multi-Field Forms — One Object, One Updater

A separate useState per field works for 1–2 fields, but it gets noisy fast. For anything bigger, keep one state object and one generic updater that merges a single field's change in.

Live form state:
{
  "plan": "free",
  "bio": "",
  "newsletter": true
}
Don't mutate the object. Always build a new object with { ...prev, [field]: value }. Writing form.bio = value directly won't trigger a re-render and can cause subtle state bugs.
5

Handling Submit

Wrap your fields in a real <form> and handle onSubmit (not the button's onClick) so pressing Enter in any field also submits. Call e.preventDefault() first — otherwise the browser does a full page reload.

6

Validation — Derive, Don't Duplicate

Resist the urge to store the error message in its own useState. Compute it directly from the field's current value on every render instead — one source of truth, no risk of the error going stale.

Pattern: only show the error after the field has been touched (usually on onBlur) so users aren't yelled at before they've finished typing.
7

When Uncontrolled Inputs Make Sense

For a simple one-off form — a search box you only read on submit, or a file input (whose value is read-only anyway) — a ref can be less code than wiring up state.

uncontrolled.jsx
import { useRef } from 'react';

export function QuickSearch() {
  const inputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("Searching for:", inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button type="submit">Search</button>
    </form>
  );
}

Note defaultValue instead of value — that's what makes this uncontrolled. React sets the initial value and then leaves the DOM alone.

8

Common Mistakes

1Mixing value and defaultValue
mistake-mixed.jsx
// ❌ BAD: React warns "a component is changing an uncontrolled input"
<input defaultValue={name} onChange={(e) => setName(e.target.value)} />

// ✅ GOOD: pick one — controlled...
<input value={name} onChange={(e) => setName(e.target.value)} />

// ...or fully uncontrolled
<input defaultValue={name} ref={inputRef} />
2Using value on a checkbox
mistake-checkbox.jsx
// ❌ BAD: checkboxes don't read "value" for their checked state
<input type="checkbox" value={agreed} onChange={...} />

// ✅ GOOD: use "checked"
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
3Forgetting preventDefault
mistake-submit.jsx
// ❌ BAD: browser does a full page reload on submit
const handleSubmit = () => {
  saveForm();
};

// ✅ GOOD
const handleSubmit = (e) => {
  e.preventDefault();
  saveForm();
};
9

Best Practices

Prefer controlled inputs — reach for uncontrolled only for simple one-off fields
One state object for multi-field forms, one generic updater function
Always call e.preventDefault() in onSubmit
Derive validation errors from state — don't store them separately
Show errors after onBlur, not on every keystroke
10

Input Type Reference

ElementValue PropChange HandlerNotes
<input type="text">valueonChange → e.target.valueMost common case
<input type="checkbox">checkedonChange → e.target.checkedNot value!
<input type="radio">checkedonChange → e.target.valueGroup by shared name
<select>valueonChange → e.target.valueWorks like a text input
<textarea>valueonChange → e.target.valueNo children, use value
11

Practice Project

Build a small contact form that:

  • Has name, email, message (textarea), and a "subscribe to updates" checkbox
  • Uses one state object with a generic updateField updater
  • Shows a validation error if email is missing an @, only after onBlur
  • Calls e.preventDefault() on submit and shows a "Message sent!" success state
12

What's Next?

🚀 Next Learning Topics:
  • Conditional Rendering: show validation errors and success states the right way
  • useState: deeper patterns for objects and arrays in state
  • useContext: share form state across nested field components
  • React Hooks Deep Dive: extract a reusable useForm custom hook

Keep practicing! 💪 Forms are one of the most common things you'll build in React — the controlled-input pattern shown here scales from a single search box to a full multi-step checkout flow.

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.