React forms: controlled vs uncontrolled inputs, and which to use
Mosaddik Billah 3 min read

On this page
Every React form makes one choice for each input: does React keep track of the value, or does the browser? Those are controlled and uncontrolled inputs. Both are correct; they suit different forms. This guide shows each, when to pick which, and how to handle submitting and errors.
Controlled inputs: React holds the value#
function NewsletterForm() {
const [email, setEmail] = useState("");
return (
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
}The input shows whatever is in email, and every key press updates email. React state is the single source of truth.
Because you always have the current value, you can react to it while the person types. With a password field set up the same way:
// with email and password both in state, as above
const tooShort = password.length > 0 && password.length < 8;
const canSubmit = email.includes("@") && password.length >= 8;
return (
<>
{tooShort && <p className="hint">At least 8 characters.</p>}
<button disabled={!canSubmit}>Create account</button>
</>
);Use controlled inputs when the UI depends on the value as it changes: live validation, a character counter, formatting a phone number while typing, or fields that appear depending on an earlier answer.
Uncontrolled inputs: the browser holds the value#
function ContactForm() {
function handleSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log(data); // { name: "...", email: "...", message: "..." }
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" defaultValue="" />
<button>Send</button>
</form>
);
}No state at all. The browser keeps each value, and you read them all at once on submit with FormData, using each input’s name. Use defaultValue (not value) to give an uncontrolled input a starting value.
Use uncontrolled inputs when you only need the values on submit: contact forms, sign-up forms, search boxes that run on Enter. It’s less code, and the component doesn’t re-render on every key press.
Don’t switch between the two#
A common warning: “A component is changing an uncontrolled input to be controlled.” It happens when value starts as undefined and later becomes a string:
const [name, setName] = useState(); // undefined
<input value={name} onChange={(e) => setName(e.target.value)} />Fix: start with an empty string, useState(""). With data loaded from an API, use value={user.name ?? ""}.
Submitting and handling errors#
Whichever kind you use, the submit handler follows the same steps: stop the page reload, check the values, send them, and show what happened.
function ContactForm() {
const [status, setStatus] = useState("idle"); // idle | sending | sent | error
async function handleSubmit(e) {
e.preventDefault();
const form = e.currentTarget;
setStatus("sending");
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
if (!res.ok) throw new Error(`Status ${res.status}`);
form.reset();
setStatus("sent");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={status === "sending"}>
{status === "sending" ? "Sending…" : "Send"}
</button>
<p role="status">
{status === "sent" && "Thanks! I'll reply soon."}
{status === "error" && "Something went wrong. Please try again."}
</p>
</form>
);
}Disabling the button while sending prevents double submissions, and role="status" makes screen readers announce the result. The res.ok check matters because fetch doesn’t throw on server errors, as explained in JavaScript fetch explained.
Use the browser’s validation first#
Attributes like required, type="email", minLength and pattern work in React too, and the browser shows its own messages with no extra code. Add your own checks on top for rules the browser can’t know, and always check again on the server, because anything in the browser can be bypassed.
What about form libraries?#
For long forms with many fields, nested data or complex rules, a library like React Hook Form (which uses uncontrolled inputs underneath) saves a lot of code. For a contact or sign-up form, plain React as shown here is enough.
I build React interfaces, including dashboard forms, as part of Wix CLI apps.
Comments