Skip to the solution
MediumFrontend6 min read

How do you handle forms in React?

640 views

The problem

Question presented to candidate: "Walk me through building a form in React. Controlled or uncontrolled, and how has React 19 changed this?"

What a strong answer should cover:

  • Controlled: the input value comes from state and every keystroke goes through onChange. React state is the source of truth.
  • Uncontrolled: the DOM holds the value; you read it on submit with a ref or FormData. defaultValue seeds it.
  • Controlled is the default because it enables live validation, conditional enabling, and formatting as you type β€” at the cost of a re-render per keystroke.
  • Uncontrolled is genuinely better for large or simple forms where you only need values at submit time; FormData reads the whole form with no state at all.
  • React 19 additions: a function passed to <form action>, plus useActionState for the result and pending state, and useFormStatus for a child to read the parent form's pending state.
  • useFormStatus ships from react-dom, not react.
  • Always give inputs a name β€” that is what FormData and form actions key off.
  • For anything non-trivial, a form library (React Hook Form, TanStack Form) exists because validation, arrays, and error handling get repetitive.
  • Accessibility: label association, and errors announced rather than only coloured.

Clarifying questions expected:

  • "Do we need validation as the user types, or only on submit?" β€” that decides controlled versus uncontrolled.
  • "Are we on React 19 with a framework, so form actions are available?"

Code / implementation expected: Yes β€” a controlled field and an uncontrolled form read via FormData.

  • forms
  • state management
  • events
  • controlled components

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactControlled with live validation, uncontrolled via FormData, and a React 19 action
import { useState, useActionState } from "react";
import { useFormStatus } from "react-dom";   // note: react-dom, not react

// ── CONTROLLED: state is the truth, so validation can run as you type. ─────
function ControlledField() {
  const [email, setEmail] = useState("");
  const valid = /.+@.+\..+/.test(email);

  return (
    <div>
      <label htmlFor="c-email">Email (controlled): </label>
      <input
        id="c-email"
        value={email}                                  // value comes FROM state
        onChange={(e) => setEmail(e.target.value)}     // every keystroke goes back
        aria-invalid={email !== "" && !valid}
        aria-describedby="c-email-err"
      />
      <button disabled={!valid}>submit</button>
      <p id="c-email-err" style={{ fontSize: 13, color: valid ? "#161" : "#a33", margin: "4px 0" }}>
        {email === "" ? "type to see live validation" : valid ? "looks valid" : "not a valid email"}
      </p>
    </div>
  );
}

// ── UNCONTROLLED: no state at all. FormData reads every named field. ───────
function UncontrolledForm() {
  const [captured, setCaptured] = useState(null);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();                            // classic handlers need this
        setCaptured(Object.fromEntries(new FormData(e.target)));
      }}
    >
      {/* name is required β€” FormData keys off it */}
      <input name="email" defaultValue="ada@example.com" />{" "}
      <input name="role" defaultValue="engineer" />{" "}
      <button type="submit">read with FormData</button>
      {captured && (
        <pre style={{ fontSize: 12, background: "#f6f6f6", padding: 8, borderRadius: 6 }}>
          {JSON.stringify(captured, null, 2)}
        </pre>
      )}
    </form>
  );
}

// ── REACT 19: a form action. No onSubmit, no preventDefault, no loading state.
function SubmitButton() {
  // Reads the ENCLOSING form's pending state β€” no prop threading.
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? "saving…" : "save"}</button>;
}

function ActionForm() {
  const [result, formAction, isPending] = useActionState(
    async (previous, formData) => {
      await new Promise((r) => setTimeout(r, 600));    // pretend network
      return "saved: " + formData.get("name");
    },
    "not submitted",
  );

  return (
    <form action={formAction}>
      <input name="name" defaultValue="Grace" />{" "}
      <SubmitButton />
      <p style={{ fontSize: 13, margin: "4px 0", color: isPending ? "#666" : "#161" }}>
        {isPending ? "pending…" : result}
      </p>
    </form>
  );
}

function Panel({ title, children }) {
  return (
    <section style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 }}>
      <h4 style={{ margin: "0 0 8px" }}>{title}</h4>
      {children}
    </section>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 500 }}>
      <Panel title="Controlled β€” re-renders per keystroke, validates live">
        <ControlledField />
      </Panel>
      <Panel title="Uncontrolled β€” zero state, read once on submit">
        <UncontrolledForm />
      </Panel>
      <Panel title="React 19 form action β€” pending state handled for you">
        <ActionForm />
      </Panel>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 53 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

When would you choose a class component over a functional component with hooks?

Continue