FormsFRAMEWORK-SPECIFICDEVICE-SPECIFICGENERAL

Controlled vs Uncontrolled Inputs

Either your framework owns every keystroke or the DOM does and you read at submit. A real trade-off between per-keystroke render cost and reactive capability — not a rule.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Should the current value of this field live in framework state, or in the DOM until I need it?

The user intent

Someone types into a field and expects the characters to appear as fast as they type them — with a live character count, a formatted phone number or a filtered list keeping up if the design promised one.

The obvious build

Put every field in state. value={x} and onChange={e => setX(e.target.value)} on every input; that is the documented way, it makes the value available everywhere, and it is consistent across the form.

Why it breaks

Every keystroke becomes a state update and a render of whatever subtree owns that state. On a form that keeps all fields in one object at the top, that is the whole form re-rendering per character (What a Component Costs to Render).

How it breaks in a real browser
  • Every keystroke becomes a state update and a render of whatever subtree owns that state. On a form that keeps all fields in one object at the top, that is the whole form re-rendering per character (What a Component Costs to Render).
  • If an expensive sibling shares the state owner — a large table, a chart, a map — it re-renders per character too, and the caret visibly lags behind the typing on a mid-range phone (Interaction Responsiveness).
  • Autofill writes several values at once without keystrokes. Controlled inputs whose state did not update can visibly snap back to the old value on the next render (Input Types, Inputmode and Autocomplete).
  • A slow render between keypress and paint can reorder characters or drop the caret to the end of the field, because the value written back is stale relative to what the user has typed since.
  • IME composition — Japanese, Chinese, Korean input — passes through intermediate states that a controlled input can normalise or clear mid-composition, making the field unusable in those languages.
  • The opposite dogma breaks too: an uncontrolled form cannot show a live character counter, cannot disable submit as validity changes, and cannot filter a list as you type, because nothing is watching the value.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • An input element always has a value in the DOM. The question is only whether the framework asserts authority over it.
  • Controlled means the rendered value prop is the state, and every keystroke fires a change handler that sets state, which re-renders, which writes the value back. The DOM value is a projection of state, and a render that does not update it will overwrite what the user typed.
  • Uncontrolled means the element has a defaultValue and thereafter owns its own value. You read it when you need it — via a ref, or via new FormData(form) at submit — and the framework does not re-render on typing at all.
  • The input event fires per character; change fires when the value is committed (blur for text, immediately for checkboxes and selects). Frameworks differ in which one they surface under a name like onChange, which is a real source of confusion when moving between them.
  • The terms are React's. Other frameworks make different structural choices: Vue's v-model is two-way binding compiled to a value binding plus an event handler, but its reactivity is fine-grained so only the dependents of that value update. Svelte compiles bind:value to direct DOM assignments with no virtual DOM diff. Solid's signals update exactly the DOM nodes that read them, so the component function does not re-run. Angular's ngModel / reactive forms integrate with change detection, and OnPush plus signals narrows what is checked.
  • The cost that varies is what re-runs per keystroke, and that is a property of the reactivity model, not of the word "controlled" (Reactivity Models).

What this makes the browser do

And which of it is avoidable.

  • The browser's own work per keystroke is the same either way: dispatch the event, update the DOM value, update the caret, style and paint the changed text.
  • Controlled adds application work between the event and the frame — the handler, the state update, the reconciliation, the DOM write-back — all on the main thread, all inside the interaction that the user is timing (What the Main Thread Owns).
  • Re-rendering a subtree does not necessarily touch the DOM. A virtual-DOM framework may diff a thousand elements and mutate none, which costs JavaScript time but no layout or paint (Reconciliation and Keys).
  • Uncontrolled adds essentially nothing per keystroke, and moves the work to submit, where a single FormData construction reads the whole form at once.
  • Where controlled costs real rendering work rather than only script time is when the state drives something visible — a filtered list, a validity style, a progress bar — because that is a genuine DOM change per character.

The question is what reacts, not which is correct

Framed as "which is better", this has no answer. Framed as "what has to react to this value while it is being typed", it usually answers itself in one sentence per field.

Notice that the decision is per field, not per form. Treating it as a form-wide policy is what produces both of the failure shapes: a fully controlled form that re-renders a page per character, and a fully uncontrolled form with a bolted-on subscription for the one field that needed to be live.

Where should this field's value live?

What needs to observe this value between keystrokes?

Uncontrolled — DOM owns it, read at submit

when Nothing observes it while typing. Addresses, names, notes, most of a profile or checkout form.

cost No live derived state. Adding one later means changing the field's model, and testing reads DOM values rather than application state.

Controlled, state scoped to the field

when Something local reacts per character: a counter, a strength meter, an inline format hint.

cost A render per keystroke of that field's component. Cheap if the component is small, and it stops being cheap silently as the component grows.

Controlled, state scoped to the form

when Cross-field rules or dependent fields need the whole picture as it changes.

cost Every keystroke renders every field unless memoised, and memoisation is its own maintenance burden (Memoization).

Controlled input, debounced derived work

when The value drives a request or an expensive computation — search-as-you-type, live preview.

cost Two values in flight, the displayed one and the settled one, and every consumer must be clear about which it reads (Derived State).

Uncontrolled with a targeted subscription

when One field in a large uncontrolled form needs live behaviour.

cost A second mechanism in the same form; readers have to know which fields are wired and which are not.

What a keystroke actually costs

The per-keystroke cost table below separates the browser's work, which is fixed, from the application's work, which is the part you chose. The distinction matters because "controlled inputs cause layout" is false, while "controlled state driving a filtered list causes layout" is true and is a different decision.

Read maybe as genuinely conditional rather than evasive: whether a render reaches the DOM at all depends on what changed, and whether a DOM change reaches layout depends on which properties it touched.

One keystroke in a text field
ChangestylelayoutpaintcompositeWhy
Typing in an uncontrolled inputyesmaybeyesyesThe browser updates the value and repaints the text. Layout only if the field grows — an auto-sizing textarea, for instance. No application code runs.
Controlled input, state scoped to the fieldyesmaybeyesyesSame browser work plus a small render. The added cost is JavaScript time inside the interaction, not extra pipeline stages.
Controlled input, state at the top of a large formyesmaybeyesyesStill the same pipeline stages, but the render diffs the entire form per character. The symptom is a delayed frame, not extra layout.
Controlled state driving a filtered listyesyesyesyesNow the DOM genuinely changes: rows are added and removed, so layout and paint are unavoidable. This is where debouncing the derived work pays (List Virtualization).
Validity class toggling on `:user-invalid`yesmaybeyesnoA colour or border change repaints; layout only if the error message changes the element's box.
Error message appearing below the fieldyesyesyesyesInserting a node reflows what follows it. Reserving the space up front avoids the content shift (Visual Stability).

caveat Every row assumes the field is in normal flow with nothing unusual around it. A fixed-position sibling, a container query, or a parent with layout containment can change layout, paint and composite answers (CSS Containment).

The same choice in five frameworks

FRAMEWORK-SPECIFICEach row describes that framework's default binding model. Escape hatches exist in all of them — React refs, Vue template refs, Angular template-driven forms — so the row describes the common path, not the only one.

The trade-off exists in every framework, but the per-keystroke cost differs because the reactivity models differ. That is why "controlled inputs are expensive" is advice with a framework attached, and why porting a rule between stacks produces confident, wrong conclusions.

This table is not a ranking. Each row describes a different set of trade-offs that the framework made deliberately, and each choice buys something elsewhere — compile-time work, runtime flexibility, or a simpler mental model (Choosing a Framework).

  • In every one of them, FormData at submit still works, because the DOM still holds the values.
  • In every one of them, autofill writes values without keystrokes, so value-change events are the reliable hook.
  • The word "controlled" only maps cleanly onto React. Elsewhere, ask "what re-runs when this value changes" and answer that instead.
FrameworkThe bindingWhat re-runs per keystrokeWhere it bites
Reactvalue + onChange, or defaultValue + refThe owning component function and its subtree, then a diffState scoped too high; the whole form re-renders per character (The React Mental Model)
Vuev-model, compiled to a binding plus a handlerOnly the reactive effects that depend on that refDeep reactive objects making unexpected dependents update (The Vue Mental Model)
Sveltebind:value, compiled to direct DOM assignmentThe generated update for that binding onlyTwo-way binding making the data flow direction hard to follow (The Svelte Mental Model)
SolidSignal read in the JSX; the component runs onceOnly the DOM expressions that read the signalDestructuring a signal, which breaks tracking silently (The Solid Mental Model)
AngularReactive forms, or ngModelChange detection over the component tree unless narrowedDefault change detection checking far more than changed (The Angular Mental Model)

How to build it

Most important first.

  • Ask what needs to react to the value as it changes. Nothing → uncontrolled. A live counter, live filtering, live validity, dependent fields, or formatting-as-you-type → controlled, scoped tightly.
  • Scope the state to the smallest component that needs it. A single formState object at the top of a large page is the main reason controlled inputs get a reputation for being slow; per-field state is usually enough.
  • For search-as-you-type, keep the input controlled and locally responsive, then debounce the derived work — do not debounce the value the input displays, or typing itself becomes laggy (Five Components, One Request).
  • Prefer FormData at submit for large, plain forms. It reads every named control at once, works with file inputs, and needs no per-field wiring (Submission: Method, Encoding and Doing It Once).
  • Keep name attributes even on controlled inputs, so FormData and autofill keep working and a submit path remains available that does not depend on state being correct.
  • Handle autofill and paste by reading value-change events rather than keyboard events, in either mode.
  • For IME languages, either leave the field uncontrolled or avoid transforming the value during composition; compositionstart / compositionend bracket the period where intermediate values must be left alone.
  • Mixed forms are fine and common: a controlled password field with a live strength meter next to five uncontrolled address fields is the correct design, not an inconsistency to clean up.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A controlled input that lags is an accessibility problem before it is a performance problem: input latency disproportionately affects users with motor impairments, users of switch or dictation input, and anyone on an older device.
  • Do not re-render in a way that recreates the input element. Focus and caret position are lost, and a screen reader treats it as a new control and re-announces it (Focus Management).
  • Live feedback driven by controlled state — character counters, validity hints — must be associated with the field via aria-describedby and updated politely, not announced on every character (Live Regions and Announcement).
  • Value transformations that fight the user, such as trimming or reformatting mid-entry, are worse with a screen reader, where the user cannot see the correction and hears an unexpected value read back.
  • IME composition handling is an accessibility and internationalisation requirement, not an optimisation: a form that mangles Japanese input excludes those users entirely.

What can go wrong

Failure modes
  • A controlled input with a value but no change handler. It becomes read-only — the user types and nothing appears — and it is the single most common first bug in this area.
  • Switching an input between controlled and uncontrolled mid-life, usually by initialising state to undefined and then setting it. React warns; other frameworks may not, and the field loses its value.
  • Formatting on every keystroke — inserting spaces into a card number, currency separators into an amount — which moves the caret to the end of the field on every character. Correct caret restoration after reformatting is genuinely difficult.
  • Debouncing the state that feeds the input's own value. The field now updates a beat behind the user, which feels broken in a way that is hard to describe in a bug report.
  • Reading a ref's value in an effect that runs before the browser has committed autofill, getting an empty string, and concluding the form is empty.
  • Uncontrolled fields inside a list that is reordered or re-keyed. The DOM values follow the nodes, so a bad key makes the values appear to swap rows (Reconciliation and Keys).
  • A server response overwriting a field the user is currently editing, because the update path treats server data as authoritative without checking focus (Server State Is Not Your State).
What can arrive out of order
  • A keystroke arriving while a render triggered by the previous keystroke is still in flight, so the value written back is one character behind what the user has typed.
  • Autofill setting several values in one turn while a re-render is pending, so the render overwrites the filled values with stale state.
  • A background refetch resolving with server data while the user is mid-edit, overwriting the field under the caret (Stale-While-Revalidate).
  • IME composition events interleaving with controlled value updates, so a normalising transform runs against a partial composition.
Security
  • Neither mode changes what the server may believe. The DOM value, the state value and the request body are all under the user's control (What the Frontend Is Responsible For in Auth).
  • Controlled state keeps values in framework memory, where they may be captured by error reporters, dev tools extensions and state-snapshot logging. Passwords and card data in a serialised state tree are a real exfiltration path (Session Replay and the Privacy It Costs).
  • Uncontrolled values live only in the DOM, which is not automatically safer — session replay and third-party scripts read the DOM too — but does keep them out of application state snapshots by default.
  • Rendering a submitted value back into the page is the same XSS question in both modes; the framework's escaping is what protects you, and dangerouslySetInnerHTML or its equivalents remove that protection (Cross-Site Scripting).
Misreads
  • "Controlled is the correct way; uncontrolled is legacy." Both are supported, documented and appropriate. Uncontrolled is what the platform does; controlled is what you opt into when you need reactivity.
  • "Controlled inputs are slow." Controlled inputs are cheap. Re-rendering an expensive subtree per keystroke is slow, and that is a state-scope decision you made separately.
  • "Uncontrolled means no validation." Native constraint validation works perfectly on uncontrolled inputs, and checkValidity() is available at any moment (Native Validation and Its Limits).
  • "This distinction is universal." The words are React's. Vue, Svelte, Solid and Angular all bind values, but what re-runs per keystroke differs enough that advice does not port unchanged (Reactivity Models).
  • "Two-way binding is controlled." v-model and ngModel look like two-way binding and compile to a one-way binding plus a handler. The syntax hides the direction; it does not remove it.
  • "Refs are an escape hatch to avoid." Reading a DOM value at submit is the platform's own model, not a workaround.

Measuring it, and what changes in the field

How you would see this
  • The Performance panel's interaction records: type into the field, look at the main-thread work between keydown and the next frame. If it is dominated by your framework's render, the state is scoped too widely (Interaction Responsiveness).
  • The framework's own profiler — React DevTools' Profiler, Vue DevTools, Angular's change-detection profiling — to see which components re-rendered per keystroke and why.
  • CPU throttling in devtools. Per-keystroke render cost is exactly the sort of thing that is invisible at full speed and obvious at 4–6x slowdown (Measure Before Optimising).
  • Field-level input latency from real users, which is the only way to know whether the median device is keeping up (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a fast desktop, a controlled form of any realistic size feels fine. The decision only becomes visible on a mid-range phone or with an expensive sibling in the same render scope.
  • Form size matters less than render scope: 40 fields with per-field state can be cheaper per keystroke than 6 fields sharing one object at the top of the page.
  • Long, mostly-static forms — profiles, settings, checkouts — favour uncontrolled. Highly reactive forms — filters, builders, anything with dependent fields — favour controlled.
  • On a slow network, server-driven updates arrive while the user is typing, and a controlled field that accepts them unconditionally overwrites in-progress edits (State Synchronization).
  • Under a fine-grained reactivity model, the per-keystroke cost of a bound value is close to the uncontrolled cost, which genuinely changes where the trade-off sits.
What this costs
  • Uncontrolled forms give up live derived state. Anything that must react per character has to be added back, usually as a targeted subscription that reintroduces some of the cost.
  • Controlled forms give up nothing in capability and pay per keystroke. Scoping state narrowly recovers most of the cost, at the price of more components and more plumbing.
  • Mixed forms are the pragmatic answer and are harder to reason about, because "where does this value live" is now a per-field question a reader has to check.
  • FormData at submit gives you strings and files with no types. Parsing and coercion move to one place — which is arguably where they belonged — but they do not disappear (Parse, Do Not Validate).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • FRAMEWORK-SPECIFICThe words "controlled" and "uncontrolled" are React vocabulary. Vue expresses the same choice as v-model versus a ref on the element; Svelte as bind:value versus reading the node; Solid as a signal-bound value versus an uncontrolled input; Angular as reactive forms versus template refs. The choice exists everywhere; the cost per keystroke differs because the reactivity models differ.
  • DEVICE-SPECIFICPer-keystroke render cost is invisible on a modern laptop and clearly visible on a mid-range phone, where the same JavaScript takes several times longer; any conclusion drawn on a development machine understates the gap.
  • GENERALThe underlying platform behaviour — the element owns a value, input fires per character, change fires on commit, FormData reads named controls — is identical in every framework and in none.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Concurrencyui-concurrency
Domains that do not exist yet
  • Programming Languages & Runtime Internals — the per-keystroke cost of a reactive binding is ultimately a question about closures, dependency tracking and allocation in the JavaScript engine underneath the framework.