Forms, validation & UX contracts
Forms are where UI state, accessibility, security, backend contracts, and user trust meet. A senior frontend answer treats forms as workflows, not input collections.
Core details
| Concern | Strong default | Failure mode |
|---|---|---|
| Ownership | Controlled for complex interactive state; uncontrolled/native for simple forms | two sources of truth |
| Validation | Client for fast feedback, server for authority | trusting client validation |
| Timing | validate on submit, blur, or change based on task cost | noisy errors while user is typing |
| Errors | field-level + form-level + focus management | red text not associated with inputs |
| Submit | pending state, disabled duplicate action, retry model | double charge / duplicate record |
| Accessibility | labels, descriptions, aria-invalid, aria-describedby | screen reader user cannot find errors |
| Optimistic UI | only when rollback semantics are clear | lying about failed writes |
Controlled vs uncontrolled: controlled inputs make React state the source of truth and are useful for dependent fields, masks, live previews, and complex validation. Uncontrolled inputs use the DOM as the source of truth and are often simpler for traditional submit flows.
Validation layering: client validation improves speed and clarity; server validation is the security and correctness boundary. Server responses should use stable error shapes that the UI can map to fields.
Async submits: use a single in-flight model, idempotency keys for dangerous writes, and explicit success/failure states. Disabling the submit button alone is not enough because users can reload, double-click before state updates, or retry after a timeout.
Understanding
Good forms reduce uncertainty. Users need to know what is required, what went wrong, what is being saved, whether they can leave, and whether the result succeeded. That means form state must be visible, accessible, and resilient to network ambiguity.
Validation timing is a product decision. Password rules can update as the user types. Credit-card payment failures should wait for submit or provider response. A long enterprise form may validate sections on blur and summarize all errors on submit.
Optimistic UI is appropriate when the user action is likely to succeed and rollback is understandable. It is risky for irreversible or regulated actions. For financial, security, or inventory-sensitive writes, prefer pending states and confirmed success.
Practical examples
Accessible field error:
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email ? <p id="email-error">{errors.email}</p> : null}Server-friendly error shape:
type FormResult =
| { ok: true; id: string }
| {
ok: false;
formError?: string;
fieldErrors?: Record<string, string>;
};Submit flow:
- Generate or reuse an idempotency key for dangerous writes.
- Disable repeat submit affordance and show pending state.
- Keep field values stable while pending.
- On validation failure, move focus to the first invalid field or error summary.
- On ambiguous network failure, explain whether retry is safe.
Senior understanding
| Probe | Strong answer |
|---|---|
| “Controlled or uncontrolled?” | Choose by state complexity, performance, and integration needs |
| “Client validation enough?” | No; server validates all authoritative rules |
| “Optimistic submit?” | Only with rollback/error model and product permission |
| “Duplicate submit?” | Idempotency key plus server-side dedupe for risky writes |
| “A11y done?” | Labels, error association, focus, keyboard, live status |
Failure modes
- Showing errors only through color.
- Clearing all fields after a failed submit.
- Using placeholder text as the only label.
- Running expensive validation on every keystroke without debouncing.
- Letting a slow failed response overwrite a newer successful submit state.
- Trusting disabled buttons as duplicate-write prevention.
Diagram
See also
Spotted something unclear or wrong on this page?