Forms are where most user input enters an app, and the controlled/uncontrolled distinction shapes how you handle it.
There are two ways to wire an input to React.
A controlled input gets its value from state and reports changes through onChange:
const [name, setName] = useState("");
<input value={name} onChange={e => setName(e.target.value)} />React state is the single source of truth, so you can validate as the user types, format input, disable the submit button, or reset the field by setting state. Checkboxes use checked and e.target.checked; <select> and <textarea> use value just like inputs.
An uncontrolled input keeps its own value in the DOM. You give it an initial defaultValue (or defaultChecked) and read it when you need it, typically on submit with FormData:
function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
save(data.get("email"));
}Uncontrolled forms are less code and work well when you only care about the values at submit time. In React 19 you can also pass a function to the form's action prop; React calls it with the FormData and resets the form afterwards. That's the basis of Actions, covered at the Advanced level.
Gotchas:
- Setting
valuewithoutonChangemakes a read-only input (React warns). - Don't switch an input between controlled and uncontrolled:
value={undefined}thenvalue="x"triggers a warning. Initialise state to"", notundefined. - Every input should have a
name(for FormData) and a<label>for accessibility.
Example
import { useState } from "react";
export default function SignupForm() {
const [email, setEmail] = useState("");
const [agree, setAgree] = useState(false);
const valid = email.includes("@") && agree;
function handleSubmit(e) {
e.preventDefault(); // no page reload
const data = new FormData(e.currentTarget);
console.log(Object.fromEntries(data)); // { email, plan, agree }
}
return (
<form onSubmit={handleSubmit}>
<label>
Email
<input name="email" value={email} onChange={e => setEmail(e.target.value)} />
</label>
<label>
Plan
<select name="plan" defaultValue="free"> {/* uncontrolled */}
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
</label>
<label>
<input type="checkbox" name="agree" checked={agree}
onChange={e => setAgree(e.target.checked)} />
I agree
</label>
<button disabled={!valid}>Sign up</button>
</form>
);
}Remember
- Controlled:
value+onChangebound to state; React state is the source of truth - Uncontrolled:
defaultValueand read values on submit withnew FormData(form) - Call
e.preventDefault()inonSubmit(or use the React 19actionprop)
Watch out for
- Initialising controlled-input state to
undefined, which flips the input from uncontrolled to controlled.