REACT Contents

Forms and Controlled Components

Controlled forms keep input state predictable. Learn scoped state, validation strategy, safe submit orchestration, and common production failures like lag and double submit.

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.