Controlled vs Uncontrolled
Every form input in React is either controlled—its value lives in React state and flows through onChange—or uncontrolled, where the DOM owns the value and you read it only when you need it via a ref or FormData. Both are first-class patterns, and the right choice depends on how much you need to observe and react to each keystroke versus simply collecting the final values on submit. Understanding the trade-off keeps your forms simple where they can be and powerful where they must be.
The two models side by side
A controlled input binds value to state and updates that state on every change, making React the single source of truth. An uncontrolled input seeds the DOM with defaultValue (or defaultChecked) and lets the browser track edits; you pull the current value from a ref when something actually needs it, such as on submit.
import { useState, useRef } from "react";
// Controlled: React owns the value.
function ControlledName() {
const [name, setName] = useState("");
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
// Uncontrolled: the DOM owns the value; a ref reads it on demand.
function UncontrolledName() {
const inputRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
console.log(inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<input defaultValue="" ref={inputRef} />
<button type="submit">Save</button>
</form>
);
}
The controlled version re-renders on every keystroke; the uncontrolled one renders once and stays quiet until you ask for its value.
When each fits
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Value lives in | React state | The DOM node |
| Initial value | value | defaultValue / defaultChecked |
| Read current value | State variable | ref.current.value or FormData |
| Re-renders per keystroke | Yes | No |
| Live validation / formatting | Easy | Awkward |
| Conditional disable, derived UI | Easy | Manual |
| Boilerplate | More | Less |
| Best for | Interactive forms, instant feedback | Simple forms, large inputs, file uploads |
Reach for controlled inputs when you need to validate as the user types, enable or disable a button based on input, transform values (uppercase, digits only), or show a live preview. Reach for uncontrolled inputs when the form is simple, when you only care about values at submit time, or for <input type="file">, which is always uncontrolled because its value is read-only and managed by the browser.
A ref to a DOM element is
nulluntil React attaches it after the first render. Always readref.current.valueinside an event handler or effect—never during render—or you will hit a null reference.
Reading uncontrolled values
You have two ways to retrieve uncontrolled values on submit. The first is a ref per field; the second, often cleaner for many fields, is the native FormData API, which reads every named input in one shot.
function ProfileForm() {
function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
const values = Object.fromEntries(data.entries());
console.log(values);
}
return (
<form onSubmit={handleSubmit}>
<input name="firstName" defaultValue="Ada" />
<input name="email" type="email" defaultValue="" />
<input name="newsletter" type="checkbox" defaultChecked />
<button type="submit">Submit</button>
</form>
);
}
Output:
{ firstName: "Ada", email: "[email protected]", newsletter: "on" }
FormData keys come from each input’s name attribute, and unchecked checkboxes are simply absent from the result—so test with data.has("newsletter") if you need a strict boolean. This approach scales to large forms without a piece of state or a ref per field.
Mixing the two
A single form can combine both styles: keep most fields uncontrolled for simplicity, but make the one field that needs live behavior controlled. Below, the password is controlled so we can show a strength meter on every keystroke, while the rest is collected via FormData.
import { useState } from "react";
function SignupForm() {
const [password, setPassword] = useState("");
const strong = password.length >= 8;
function handleSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log({ ...data, password });
}
return (
<form onSubmit={handleSubmit}>
<input name="username" defaultValue="" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<p>{strong ? "Strong password" : "Too short"}</p>
<button type="submit" disabled={!strong}>
Create account
</button>
</form>
);
}
The one rule that must not be broken: a given input is controlled or uncontrolled for its entire lifetime. Switching value from undefined to a string (or back) flips an input between modes and triggers a React warning.
Never let
valueflip between a defined string andundefined/null. Initialize controlled state to"", notundefined, so the input is controlled from the very first render.
Best Practices
- Default to controlled inputs when you need validation, formatting, or derived UI on every keystroke.
- Prefer uncontrolled inputs with
FormDatafor simple forms where you only read values on submit. - Always use
defaultValue/defaultChecked(notvalue/checked) for uncontrolled fields. - Read refs only inside event handlers or effects, never during render, since
ref.currentisnullinitially. - Keep each input controlled or uncontrolled for its whole life—initialize controlled state to
""to avoid mode flips. - Remember that
<input type="file">is inherently uncontrolled; read it from a ref orFormData.