Forms in Frameworks
Date: 2026-08-19
A form already works. It collects named values, validates them, and posts them to a URL without a line of JavaScript. Most framework form code is spent rebuilding that behaviour badly, and the tell is a form with no
actionand nonameattributes.
Controlled means the framework holds the value and the input displays it. Uncontrolled means the DOM holds the value and the framework reads it when it needs to.
// CONTROLLED — framework is the source of truth
<input value={email} onChange={e => setEmail(e.target.value)} />
// re-renders on every keystroke. value and display cannot diverge.
// UNCONTROLLED — the DOM is the source of truth
<input name="email" defaultValue={email} />
// no re-render. read it from the form on submit.What controlled buys, and what it charges
Buys: validation as you type, formatting on input (card numbers, postcodes), one field’s value driving another’s options, and disabling submit until the form is valid. Anything where the value must be inspected during editing needs it.
Charges: a render per keystroke — cheap for one field, not when the field sits high in a tree and re-renders a page-sized subtree, which is the usual cause of typing lag that gets blamed on the framework. And, more quietly, it makes the form JavaScript-dependent unless you were careful.
The rule that follows: control the fields that need controlling. A checkout form does not need eleven pieces of state because one field needs live validation.
The name attribute is the thing people drop
An uncontrolled form is only readable if its inputs are named, because name is what puts a value into the submitted body. Controlled forms often omit name entirely — the framework has the value, so nothing needs it — and that single omission is what makes the form incapable of working without JavaScript.
<form method="post" action="/subscribe">
<input name="email" type="email" required />
<button>Subscribe</button>
</form>That posts email=… to /subscribe with validation, keyboard handling, autofill and a working Enter key, all of it before any framework loads. Frameworks with route-level actions keep exactly this and enhance it — the same form, intercepted when JavaScript is available, posted natively when it isn’t. That’s Progressive Enhancement achieved by not removing something, which is the only version that reliably survives contact with a deadline.
Validation happens in three places, and one of them counts
BROWSER required, type="email", pattern, min/max
free, instant, localised. an accessibility win.
trivially bypassed — it is a convenience, not a check
CLIENT SCRIPT cross-field rules, async checks (is this promo code valid)
better errors, better timing. still bypassable
SERVER the only validation that exists ← the real one
must repeat everything above
Every rule enforced on the client must be enforced again on the server. Client validation is a user-experience feature; treating it as a security control is Common Vulnerabilities waiting to happen.
What the framework should be doing for you
Three things worth having, and worth checking a form library actually provides:
- Submission state — pending, error, success, without hand-rolled booleans that drift out of sync
- Optimistic display — show the result immediately, reconcile when the server answers
- Error association — the message must be tied to its input with
aria-describedby, and focus must move to the first invalid field. Rendering an error paragraph near the input is not the same thing, and screen reader users get no error at all — Forms and ARIA
The failure modes, in order of how often they appear
- No
name, noaction— the form cannot function without JavaScript, and there was no reason for it not to - Double submit — no pending state, user clicks twice, two orders. The fix is disabling on submit, and the server-side one is Idempotency
- State reset on re-render — an uncontrolled input remounted by a parent re-render loses what the user typed, because its value lived in a DOM node that no longer exists
- Validation only on submit for long forms, so the user learns about eight errors after scrolling to the bottom