React Interview Prep
Events and Forms

Form Handling Patterns

Form Handling Patterns

LinkedIn Hook

You build a React form with five inputs and write five separate onChange handlers. It works, but your code looks like it was written by a copy-paste machine.

Then someone asks in an interview: "How would you handle a form with 20 fields?" and you realize your approach does not scale.

Controlled forms in React follow a pattern. One state object. One handler. Every input reads from state and writes back through the same function using e.target.name. This is the single-handler pattern, and it is the foundation of every production form you will ever build.

But the real interview questions go deeper. "When should you validate — on change, on blur, or on submit?" "What is the difference between controlled and uncontrolled form submission?" "Why would you reach for React Hook Form instead of managing state yourself?" "How do you prevent a form from submitting twice?"

These are not edge cases. They are daily decisions on every frontend team.

In this lesson, I cover controlled forms with multiple inputs, the single handler pattern, form submission with e.preventDefault(), validation strategies (on submit vs on change vs on blur), and when to reach for libraries like React Hook Form or Formik instead of rolling your own.

If you have ever built a form that "worked" but could not explain why you structured it that way — this one fills that gap.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #CodingInterview #FormHandling #ReactHookForm #100DaysOfCode


Form Handling Patterns thumbnail


What You'll Learn

  • How to build controlled forms with multiple inputs using a single state object
  • The single handler pattern — one handleChange for all fields using e.target.name
  • How to handle form submission properly with e.preventDefault() and state
  • Three validation strategies: on submit, on change, and on blur — when to use each
  • When to stop managing forms yourself and reach for React Hook Form or Formik

The Concept — Controlled Forms

Analogy: The Centralized Switchboard

Imagine you run a call center with 20 phone operators. Each operator takes calls on a different topic — billing, shipping, returns, technical support.

The naive approach: Give every operator their own independent logbook. When a manager asks "What is the status of customer #4521?", someone has to check 20 separate logbooks. Nobody has the full picture.

The centralized approach: Every operator logs calls into one shared system. When an operator takes a call, they record it under their department name in the central system. The manager can see everything in one place, run reports, and catch errors before they propagate.

React controlled forms work like the centralized system. Instead of each input managing its own state independently (uncontrolled), every input reads from one state object and writes back through one handler. The state object is the single source of truth. React always knows what every field contains, which means you can validate, transform, disable buttons, or reset the entire form from one place.

The name attribute on each input is like the department name — it tells the single handler which field in the central system to update.


Controlled Forms with Multiple Inputs

A controlled form means every input's value is driven by React state. The input displays what state tells it to display, and the input updates state when the user types. React is always in control.

Code Example 1: Single Handler for Multiple Fields

import { useState } from "react";

function RegistrationForm() {
  // One state object holds all form fields
  const [formData, setFormData] = useState({
    username: "",
    email: "",
    password: "",
    role: "developer",
  });

  // One handler updates any field using the input's name attribute
  function handleChange(e) {
    const { name, value } = e.target;
    setFormData((prev) => ({
      ...prev,        // Keep all existing fields
      [name]: value,  // Update only the field that changed
    }));
  }

  // Checkbox and select elements follow the same pattern
  function handleCheckboxChange(e) {
    const { name, checked } = e.target;
    setFormData((prev) => ({
      ...prev,
      [name]: checked,
    }));
  }

  return (
    <form>
      <input
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="Username"
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <select name="role" value={formData.role} onChange={handleChange}>
        <option value="developer">Developer</option>
        <option value="designer">Designer</option>
        <option value="manager">Manager</option>
      </select>
    </form>
  );
}

// Typing "alice" in the username field:
// formData becomes: { username: "alice", email: "", password: "", role: "developer" }
// Each keystroke triggers handleChange, which updates only the "username" key.

Key insight: The computed property name [name]: value is what makes this pattern work. The name attribute on the HTML input matches the key in the state object. One handler, any number of fields.


Form Submission

Submitting a form in React means preventing the browser's default behavior (page reload), collecting the state, and doing something with it — usually an API call.

Code Example 2: Form Submission with Loading and Reset

import { useState } from "react";

function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitResult, setSubmitResult] = useState(null);

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  }

  async function handleSubmit(e) {
    // Prevent the browser from refreshing the page
    e.preventDefault();

    // Prevent double submission
    if (isSubmitting) return;

    setIsSubmitting(true);
    setSubmitResult(null);

    try {
      // Simulate an API call
      const response = await fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(formData),
      });

      if (!response.ok) throw new Error("Submission failed");

      setSubmitResult("Message sent successfully!");

      // Reset the form after successful submission
      setFormData({ name: "", email: "", message: "" });
    } catch (error) {
      setSubmitResult("Something went wrong. Please try again.");
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Your name"
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Your email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Your message"
      />

      {/* Disable button while submitting to prevent double clicks */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Send Message"}
      </button>

      {/* Show result feedback */}
      {submitResult && <p>{submitResult}</p>}
    </form>
  );
}

// User fills in name, email, message and clicks "Send Message":
// 1. e.preventDefault() stops page reload
// 2. isSubmitting becomes true — button shows "Sending..." and is disabled
// 3. API call fires with formData as JSON body
// 4. On success: result message shown, form fields reset to empty strings
// 5. On failure: error message shown, form data preserved so user can retry

Interview detail: Preventing double submission (checking isSubmitting and disabling the button) is something interviewers notice. It shows you think about real-world UX, not just happy-path demos.


Validation Patterns — On Submit vs On Change vs On Blur

Validation is where most form implementations diverge. There are three strategies, each with different trade-offs.

Code Example 3: Three Validation Strategies

import { useState } from "react";

function ValidatedForm() {
  const [formData, setFormData] = useState({ email: "", age: "" });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // Validation logic — reusable across all strategies
  function validate(fieldName, value) {
    switch (fieldName) {
      case "email":
        if (!value) return "Email is required";
        if (!/\S+@\S+\.\S+/.test(value)) return "Email is invalid";
        return "";
      case "age":
        if (!value) return "Age is required";
        if (Number(value) < 18) return "Must be 18 or older";
        return "";
      default:
        return "";
    }
  }

  // STRATEGY 1: Validate on change — immediate feedback on every keystroke
  // Good for: short forms, fields where real-time feedback helps (password strength)
  // Bad for: fields where partial input is always "invalid" (email while typing)
  function handleChangeWithValidation(e) {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));

    // Only show errors for fields the user has already interacted with
    if (touched[name]) {
      const error = validate(name, value);
      setErrors((prev) => ({ ...prev, [name]: error }));
    }
  }

  // STRATEGY 2: Validate on blur — feedback when the user leaves a field
  // Good for: most forms. User finishes typing before seeing errors.
  // This is the most common strategy in production.
  function handleBlur(e) {
    const { name, value } = e.target;
    setTouched((prev) => ({ ...prev, [name]: true }));

    const error = validate(name, value);
    setErrors((prev) => ({ ...prev, [name]: error }));
  }

  // STRATEGY 3: Validate on submit — check everything at once
  // Good for: simple forms, wizards where you validate a whole step at once
  // Bad for: long forms where users want earlier feedback
  function handleSubmit(e) {
    e.preventDefault();

    // Validate all fields at once
    const newErrors = {};
    Object.keys(formData).forEach((field) => {
      const error = validate(field, formData[field]);
      if (error) newErrors[field] = error;
    });

    setErrors(newErrors);

    // If there are any errors, stop submission
    if (Object.keys(newErrors).length > 0) {
      console.log("Form has errors:", newErrors);
      return;
    }

    console.log("Form is valid! Submitting:", formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChangeWithValidation}
          onBlur={handleBlur}
          placeholder="Email"
        />
        {/* Only show error if the field has been touched */}
        {touched.email && errors.email && (
          <span style={{ color: "red" }}>{errors.email}</span>
        )}
      </div>

      <div>
        <input
          name="age"
          value={formData.age}
          onChange={handleChangeWithValidation}
          onBlur={handleBlur}
          placeholder="Age"
        />
        {touched.age && errors.age && (
          <span style={{ color: "red" }}>{errors.age}</span>
        )}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

// User types "abc" in email, then tabs to age field:
// 1. While typing — no error shown (field not yet touched on blur)
// 2. On blur (leaving email field) — touched.email becomes true
//    validate("email", "abc") returns "Email is invalid"
//    Error message appears: "Email is invalid"
// 3. User fixes email to "abc@test.com" — error clears on next change
// 4. On submit — all fields validated at once as a final safety net

The best practice: Combine on-blur and on-submit. Validate when the user leaves a field (so they get feedback before submitting), and re-validate everything on submit as a safety net. On-change validation works well for specific fields like password strength meters, but showing "Invalid email" while someone is still typing their address is a poor experience.

Form Handling Patterns visual 1


Form Libraries — When to Stop Rolling Your Own

For forms with more than a few fields, complex validation rules, dynamic fields, or nested objects, managing everything with useState becomes tedious. That is when form libraries earn their place.

Code Example 4: React Hook Form vs Manual State

// === MANUAL APPROACH — works for small forms ===
import { useState } from "react";

function ManualForm() {
  const [formData, setFormData] = useState({ email: "", password: "" });
  const [errors, setErrors] = useState({});

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  }

  function handleSubmit(e) {
    e.preventDefault();
    const newErrors = {};
    if (!formData.email) newErrors.email = "Required";
    if (formData.password.length < 8) newErrors.password = "Min 8 characters";
    setErrors(newErrors);
    if (Object.keys(newErrors).length === 0) {
      console.log("Submit:", formData);
    }
  }

  // You manage: state, onChange, errors, touched, submission, reset...
  // For 2 fields this is fine. For 20 fields it becomes a maintenance burden.
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" value={formData.email} onChange={handleChange} />
      {errors.email && <span>{errors.email}</span>}
      <input name="password" value={formData.password} onChange={handleChange} />
      {errors.password && <span>{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}

// === REACT HOOK FORM — scales to complex forms ===
// npm install react-hook-form
import { useForm } from "react-hook-form";

function HookFormExample() {
  const {
    register,   // Connects inputs to the form (replaces value + onChange)
    handleSubmit, // Wraps your submit handler with validation
    formState: { errors, isSubmitting },
  } = useForm();

  function onSubmit(data) {
    // "data" contains all form values — no manual state management
    console.log("Submit:", data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("email", {
          required: "Email is required",
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: "Invalid email format",
          },
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register("password", {
          required: "Password is required",
          minLength: {
            value: 8,
            message: "Must be at least 8 characters",
          },
        })}
      />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit" disabled={isSubmitting}>Submit</button>
    </form>
  );
}

// React Hook Form uses uncontrolled inputs (refs) internally.
// This means fewer re-renders — the component does NOT re-render on every keystroke.
// For a form with 50 fields, this performance difference is significant.

When to use which:

ScenarioRecommendation
1-5 simple fieldsManual useState — keep it simple
6+ fields or complex validationReact Hook Form — less boilerplate, better performance
Dynamic fields (add/remove rows)React Hook Form with useFieldArray
Schema-based validation (Zod, Yup)React Hook Form + resolver, or Formik + Yup
Legacy codebase already using FormikKeep Formik — migration cost is rarely worth it

React Hook Form vs Formik: React Hook Form is the newer library, uses uncontrolled inputs by default (fewer re-renders), has a smaller bundle size, and has largely overtaken Formik in adoption. Formik still works well but re-renders on every keystroke by default because it uses controlled inputs. For new projects, React Hook Form is the standard recommendation.

Form Handling Patterns visual 2


Common Mistakes

Mistake 1: Forgetting the name attribute on inputs

function BrokenForm() {
  const [formData, setFormData] = useState({ email: "", name: "" });

  function handleChange(e) {
    const { name, value } = e.target;
    // If the input has no "name" attribute, name is an empty string
    // This creates a key "" in your state object instead of updating the right field
    setFormData((prev) => ({ ...prev, [name]: value }));
  }

  // WRONG — missing name attribute, handler cannot identify the field
  return <input value={formData.email} onChange={handleChange} />;

  // RIGHT — name attribute matches the state key
  return <input name="email" value={formData.email} onChange={handleChange} />;
}

// Symptom: typing in the input does nothing (or creates a mystery "" key in state).
// Fix: every input using the single handler pattern MUST have a name attribute
// that matches its corresponding key in the state object.

Mistake 2: Mutating state directly instead of spreading

function MutationBug() {
  const [formData, setFormData] = useState({ email: "", password: "" });

  function handleChange(e) {
    const { name, value } = e.target;

    // WRONG — mutates the existing state object directly
    // React will not detect the change and will not re-render
    formData[name] = value;
    setFormData(formData);

    // RIGHT — create a new object with the spread operator
    setFormData((prev) => ({ ...prev, [name]: value }));
  }

  return <input name="email" value={formData.email} onChange={handleChange} />;
}

// React uses Object.is() to compare old and new state.
// If you mutate and pass the same object reference, old === new is true.
// React skips the re-render. The input appears frozen.

Mistake 3: Validating on every keystroke without debouncing or guarding with touched state

function AnnoyingForm() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  // WRONG — shows "Invalid email" immediately as user starts typing
  // Typing "j" instantly shows an error. Typing "jo" still shows an error.
  // The user sees errors before they have a chance to finish.
  function handleChange(e) {
    const value = e.target.value;
    setEmail(value);
    if (!/\S+@\S+\.\S+/.test(value)) {
      setError("Invalid email"); // Fires on every single keystroke
    } else {
      setError("");
    }
  }

  // RIGHT — validate on blur instead, or only after the field has been touched
  // See the ValidatedForm example above for the correct pattern.

  return (
    <div>
      <input value={email} onChange={handleChange} />
      {error && <span style={{ color: "red" }}>{error}</span>}
    </div>
  );
}

// Rule: Do not show validation errors for a field the user is still actively editing.
// Use onBlur or a "touched" flag to delay error display until the user moves on.

Interview Questions

Q: What is a controlled form component in React?

A controlled form component is one where React state is the single source of truth for the input's value. The input displays whatever state tells it to (value={state}), and updates state through an onChange handler. This means React always knows the current value of every field, which makes validation, conditional rendering, and programmatic form manipulation straightforward. The opposite is an uncontrolled component, where the DOM itself holds the value and you read it via refs.

Q: How do you handle multiple form inputs with a single onChange handler?

You give each input a name attribute that matches its key in the state object. The handler reads e.target.name and e.target.value, then updates state using a computed property name: setFormData(prev => ({ ...prev, [name]: value })). This scales to any number of fields without writing separate handlers. For checkboxes, you read e.target.checked instead of e.target.value.

Q: What are the three main validation strategies in React forms, and when would you use each?

On change: Validates on every keystroke. Good for real-time feedback like password strength meters. Bad for fields like email where partial input is always invalid. On blur: Validates when the user leaves a field. This is the most common production strategy because users finish typing before seeing errors. On submit: Validates everything at once when the form is submitted. Good for simple forms and as a final safety net. The best practice is to combine on-blur for immediate field-level feedback with on-submit as a fallback to catch anything missed.

Q: Why would you choose React Hook Form over managing form state manually with useState?

React Hook Form reduces boilerplate significantly — you do not need to manually manage state, onChange handlers, error objects, or touched tracking. It uses uncontrolled inputs internally (refs instead of state), so the form does not re-render on every keystroke, which improves performance in large forms. It also provides built-in validation, integration with schema validation libraries like Zod and Yup, and utilities like useFieldArray for dynamic fields. Manual useState is fine for 1-5 simple fields, but beyond that the complexity grows faster than the feature count.

Q: What is the difference between how React Hook Form and Formik manage form inputs internally?

Formik uses controlled inputs — it stores all form values in state and re-renders the component tree on every keystroke. React Hook Form uses uncontrolled inputs — it registers inputs via refs and only triggers re-renders when necessary (like when validation errors change). This makes React Hook Form faster for large forms. Formik has a larger API surface and was the dominant library for years, but React Hook Form has become the community standard for new projects due to its smaller bundle size and better render performance.


Quick Reference — Cheat Sheet

+-----------------------------------+-------------------------------------------+
| Concept                           | Key Point                                 |
+-----------------------------------+-------------------------------------------+
| Controlled input                  | value={state} + onChange={handler}.        |
|                                   | React is the source of truth, not DOM.    |
+-----------------------------------+-------------------------------------------+
| Single handler pattern            | One handleChange for all fields.           |
|                                   | Uses e.target.name + computed property    |
|                                   | [name]: value to update the right key.    |
+-----------------------------------+-------------------------------------------+
| Form submission                   | e.preventDefault() to stop page reload.   |
|                                   | Disable button during submit to prevent   |
|                                   | double clicks. Reset state on success.    |
+-----------------------------------+-------------------------------------------+
| Validate on change                | Immediate feedback per keystroke.          |
|                                   | Good for password strength. Bad for email.|
+-----------------------------------+-------------------------------------------+
| Validate on blur                  | Feedback when user leaves the field.      |
|                                   | Best default strategy for most forms.     |
+-----------------------------------+-------------------------------------------+
| Validate on submit                | Check everything at once on submit.       |
|                                   | Use as a final safety net alongside blur. |
+-----------------------------------+-------------------------------------------+
| touched state                     | Track which fields the user has visited.  |
|                                   | Only show errors for touched fields.      |
+-----------------------------------+-------------------------------------------+
| React Hook Form                   | Uncontrolled inputs via refs. Fewer       |
|                                   | re-renders. Built-in validation. Use for  |
|                                   | complex forms (6+ fields).                |
+-----------------------------------+-------------------------------------------+
| Formik                            | Controlled inputs via state. Re-renders   |
|                                   | on every keystroke. Still works, but RHF  |
|                                   | is the newer standard.                    |
+-----------------------------------+-------------------------------------------+
| name attribute                    | MUST match the state key for the single   |
|                                   | handler pattern to work. Missing name =   |
|                                   | silent bug.                               |
+-----------------------------------+-------------------------------------------+

RULE: One state object + one handler = scalable form pattern.
RULE: Validate on blur + on submit — not on every keystroke.
RULE: Use React Hook Form when manual state management gets tedious.

Previous: Lesson 5.1 — Event Handling in React -> Next: Lesson 5.3 — Debounced Input & Search Patterns ->


This is Lesson 5.2 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page