Comparisons
Pairs that get conflated in real conversations, and in real pull requests. Neither column wins — what decides is the requirement. Each record leads with the confusion, because the confusion is the reason the record exists.
Controlled vs Uncontrolled form inputs
Controlled is treated as the correct default and uncontrolled as a legacy escape hatch, which gets the cost backwards. Every keystroke in a controlled field is a state update and a render, so a large form re-renders on every character — and a caret that jumps or lags a letter behind is almost always a controlled input whose render path is too expensive or whose value was normalised asynchronously. Uncontrolled fields cost nothing per keystroke because the browser is doing what it has always done. The genuinely important point underneath is that this is a per-field decision, not a per-form one: one field that drives a live preview can be controlled inside a form whose other twenty fields are not. And "uncontrolled" does not mean unvalidated — native constraint validation runs either way, and it runs before your JavaScript has loaded.
When something outside the field depends on the value as it is typed: live validation, a dependent field, a filtered list, a character counter, a value you must normalise as it is entered.
When nothing needs the value until submission: most forms. The platform already stores it, validates it, autofills it and resets it.
| Dimension | Controlled — the value is application state and every keystroke round-trips through it | Uncontrolled — the DOM node holds the value and you read it when you need it |
|---|---|---|
| Source of truth | Application state | The DOM node |
| Cost per keystroke | A state update and a render | Nothing beyond what the browser already does |
| Live derived UI | Natural | Needs an event listener and local state anyway |
| Typical failure | Laggy caret, cursor jumps after async normalisation | Forgetting the value exists until submit, so nothing reacts |
| Resetting | Set the state | form.reset(), which the platform provides |
| Works before JS loads | No | Yes — the field is a real field |
| Scope of the decision | Per field | Per field — mixing within a form is normal, not a smell |