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

What people get wrong about this pair

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.

Controlled — the value is application state and every keystroke round-trips through it
Use it when

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.

Uncontrolled — the DOM node holds the value and you read it when you need it
Use it when

When nothing needs the value until submission: most forms. The platform already stores it, validates it, autofills it and resets it.

DimensionControlled — the value is application state and every keystroke round-trips through itUncontrolled — the DOM node holds the value and you read it when you need it
Source of truthApplication stateThe DOM node
Cost per keystrokeA state update and a renderNothing beyond what the browser already does
Live derived UINaturalNeeds an event listener and local state anyway
Typical failureLaggy caret, cursor jumps after async normalisationForgetting the value exists until submit, so nothing reacts
ResettingSet the stateform.reset(), which the platform provides
Works before JS loadsNoYes — the field is a real field
Scope of the decisionPer fieldPer field — mixing within a form is normal, not a smell