Forms and Controlled Components
On this page
Controlled vs Uncontrolled
- Controlled: input value lives in React state.
- Uncontrolled: input value lives in the DOM and is read via refs.
- Use controlled inputs for validation, conditional UI, and cross field dependencies.
Scope State to Avoid Input Lag
- Avoid one giant form object updated on every keystroke.
- Prefer per field state or a reducer split by responsibility.
- Keep expensive work out of onChange.
Validation Strategy
- On change: fast feedback, more renders.
- On blur: fewer renders, delayed feedback.
- On submit: simplest, must show field errors clearly.
- Production rule: client and server must enforce the same invariants.
Example: Controlled Field
function EmailField({ value, onChange, error }) {
return (
<div>
<label>Email</label>
<input
value={value}
onChange={(e) => onChange(e.target.value)}
inputMode="email"
/>
{error ? <div role="alert">{error}</div> : null}
</div>
);
}
Submit Orchestration
- Model status: idle → submitting → success or error.
- Disable submit while in flight to prevent double submit.
- Normalize server errors into field errors and a global message.
Example: Submit Guard + Error Normalization
function SignupForm() {
const [email, setEmail] = React.useState("");
const [status, setStatus] = React.useState("idle"); // idle|submitting|error|success
const [fieldErrors, setFieldErrors] = React.useState({});
async function onSubmit(e) {
e.preventDefault();
if (status === "submitting") return;
setStatus("submitting");
setFieldErrors({});
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (!res.ok) {
const err = await res.json();
setFieldErrors(err.fieldErrors || {});
setStatus("error");
return;
}
setStatus("success");
} catch (err) {
setStatus("error");
}
}
return (
<form onSubmit={onSubmit}>
<EmailField value={email} onChange={setEmail} error={fieldErrors.email} />
<button disabled={status === "submitting"}>
{status === "submitting" ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
Failure Modes
- Input lag from over scoped state and expensive renders.
- Double submit without an in flight guard.
- Stale errors not cleared on retry.
- Out of order responses overwriting newer UI state.
- Client server mismatch in validation rules.
Production Checklist
- Submit is guarded and server is idempotent where possible.
- Validation strategy is explicit.
- Errors are structured (field vs global).
- State scope prevents unnecessary re renders.