Forms
Controlled vs uncontrolled inputs, multiple fields, checkboxes/selects, submission, and when to reach for RHF.
Controlled inputs 🎯#
State is the single source of truth: the input's value comes from state and every keystroke flows through onChange. React re-renders on each change, so the value in state and on screen are always in sync.
function Name() {
const [name, setName] = useState("");
return <input value={name} onChange={e => setName(e.target.value)} />;
}
Uncontrolled inputs#
The DOM holds the value; you read it on demand via a ref and set an initial value with defaultValue (never value). No re-render per keystroke.
function Name() {
const inputRef = useRef(null);
function onSubmit(e) {
e.preventDefault();
console.log(inputRef.current.value); // read from the DOM
}
return (
<form onSubmit={onSubmit}>
<input defaultValue="Ada" ref={inputRef} />
</form>
);
}
Controlled vs uncontrolled#
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | The DOM |
| Value prop | value |
defaultValue |
| Read value | From state | ref.current.value |
| Re-render per keystroke | Yes | No |
| Instant validation / formatting | Easy 🟢 | Awkward |
| Best for | Most forms, dynamic UI | File inputs, quick/simple forms, integrating non-React widgets |
⚠️ Don't switch a field between controlled and uncontrolled (e.g. value={x} where x starts undefined then becomes a string) — React warns and resets cursor/behavior. Initialize state to "".
Multiple inputs with one handler#
Give each input a name and key state by it. 🟢
function Profile() {
const [form, setForm] = useState({ email: "", city: "" });
const onChange = e => {
const { name, value } = e.target;
setForm(f => ({ ...f, [name]: value })); // computed key
};
return (
<>
<input name="email" value={form.email} onChange={onChange} />
<input name="city" value={form.city} onChange={onChange} />
</>
);
}
Checkboxes & selects#
Checkboxes use checked/e.target.checked (not value). A <select multiple> yields multiple selected options.
<input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="del">Delhi</option>
<option value="blr">Bengaluru</option>
</select>
Submission#
Handle onSubmit on the <form> (fires on Enter and button click) and call e.preventDefault() to stop the full-page reload.
<form onSubmit={e => { e.preventDefault(); save(form); }}>…<button>Save</button></form>
When to reach for a library#
For large forms, complex validation, and performance, React Hook Form uses uncontrolled inputs + refs to minimize re-renders (register, handleSubmit), pairing well with schema validators like Zod/Yup. Formik is the older controlled-state alternative.
flowchart TD
A["User types"] --> B{"Controlled?"}
B -->|Yes| C["onChange -> setState"] --> D["Re-render, value = state"]
B -->|No| E["DOM stores value"] --> F["Read via ref on submit"]
Interview Q&A#
Q1. Controlled vs uncontrolled — the one-line difference?
Controlled inputs derive their value from React state (value + onChange); uncontrolled inputs keep the value in the DOM and you read it via a ref (defaultValue).
Q2. When would you prefer uncontrolled? File inputs (which are always uncontrolled), simple forms where per-keystroke state is unnecessary, integrating third-party/non-React widgets, and to avoid re-renders in very large forms.
Q3. Why does React warn about switching controlled/uncontrolled?
Passing value={undefined} then a string makes React unsure who owns the value, causing lost input state and cursor jumps. Initialize state to a defined value like "".
Q4. How do you handle many inputs cleanly?
Give each a name, store an object in state, and use a computed key: setForm(f => ({ ...f, [name]: value })).
Q5. Why is React Hook Form fast? It keeps inputs uncontrolled and subscribes via refs, so typing doesn't trigger component re-renders — only subscribed fields update.