Back to Machine Coding
Machine Coding
easy
mid

How would you build a login form with validation and state management?

Controlled inputs for email/password, validation (on blur + submit), per-field error state, submit handling with loading/error states, disabled submit while submitting. Use a <form> with onSubmit, proper input types/labels, and never trust client validation alone — the server re-validates.

5 min read·~20 min to think through

A login form is small but interviewers grade the details: validation timing, all the states, accessibility, and form semantics.

The implementation

jsx
function LoginForm({ onLogin }) {
  const [values, setValues] = useState({ email: "", password: "" });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [status, setStatus] = useState("idle"); // idle|submitting|error

  const validate = (vals) => {
    const e = {};
    if (!vals.email) e.email = "Email is required";
    else if (!/^[^@]+@[^@]+\.[^@]+$/.test(vals.email)) e.email = "Invalid email";
    if (!vals.password) e.password = "Password is required";
    else if (vals.password.length < 8) e.password = "Min 8 characters";
    return e;
  };

  const handleChange = (e) => {
    const next = { ...values, [e.target.name]: e.target.value };
    setValues(next);
    if (touched[e.target.name]) setErrors(validate(next)); // re-validate once touched
  };

  const handleBlur = (e) => {
    setTouched((t) => ({ ...t, [e.target.name]: true }));
    setErrors(validate(values));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const e2 = validate(values);
    setErrors(e2);
    setTouched({ email: true, password: true });
    if (Object.keys(e2).length) return;

    setStatus("submitting");
    try {
      await onLogin(values);
    } catch (err) {
      setStatus("error");
      setErrors({ form: "Invalid email or password" }); // generic — don't leak which
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" value={values.email}
        onChange={handleChange} onBlur={handleBlur}
        aria-invalid={!!errors.email} aria-describedby="email-err" />
      {touched.email && errors.email && <span id="email-err">{errors.email}</span>}

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" value={values.password}
        onChange={handleChange} onBlur={handleBlur} />
      {touched.password && errors.password && <span>{errors.password}</span>}

      {errors.form && <div role="alert">{errors.form}</div>}
      <button type="submit" disabled={status === "submitting"}>
        {status === "submitting" ? "Signing in…" : "Sign in"}
      </button>
    </form>
  );
}

What's being graded

  • A real <form> with onSubmit + e.preventDefault() — so Enter submits.
  • Controlled inputs, correct type (email, password), <label htmlFor> linked to id.
  • Validation timing — validate on blur first (don't yell mid-typing), then re-validate on change once the field is touched, and everything on submit.
  • All statesidle / submitting (disable button, spinner) / error.
  • Field errors inline + a form-level error for failed auth — and that error should be generic ("invalid email or password"), never "wrong password" (don't reveal which field is wrong — account enumeration).
  • Accessibilityaria-invalid, aria-describedby, role="alert" on the form error.
  • Server re-validates — client validation is UX only.

For production: a form library (React Hook Form) + schema (Zod), and proper autocomplete attributes.

The framing

"Controlled email/password inputs in a real <form> with onSubmit so Enter works. Validation timing is the nuance — validate on blur first, re-validate on change once touched, and everything on submit. I model status as idle/submitting/error, disable the button while submitting, show inline field errors plus a generic form-level error for auth failure — never 'wrong password' specifically, to avoid account enumeration. Accessibility via aria-invalid/describedby and role=alert. And client validation is purely UX — the server re-validates."

Follow-up questions

  • When should each field validate — change, blur, or submit?
  • Why should the auth-failure message be generic?
  • What ARIA attributes does an accessible form need?
  • Why use a <form> element instead of a div with a button?

Common mistakes

  • A div + button instead of a form — Enter doesn't submit.
  • Validating aggressively on every keystroke from the start.
  • Revealing whether the email or the password was wrong.
  • No loading state / not disabling the button while submitting.
  • Missing labels and ARIA — inaccessible.
  • Treating client validation as sufficient.

Performance considerations

  • Trivial scale. Controlled inputs re-render the form per keystroke — fine here; a form library's uncontrolled approach matters more for large forms.

Edge cases

  • Submitting with empty fields.
  • Server rejects after client validation passed.
  • Rapid double-submit.
  • Password managers autofilling the fields.
  • Network failure during submit.

Real-world examples

  • Every app's sign-in page.
  • React Hook Form + Zod powering production login/signup forms.

Senior engineer discussion

Seniors nail form semantics, validation timing, all states, and accessibility without prompting, make the auth error generic for security, stress server re-validation, and mention a form library + schema for production.

Related questions