Skip to solution
mediumFrontend

How do you handle forms in React?

640 views
01

Understand 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.

formsstate managementeventscontrolled components
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes state and events. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The FormData and useActionState results below were pro

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Controlled with live validation, uncontrolled via FormData, and a React 19 action
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 53 of 119 decoded in the React.js track. One more won't hurt.

Back to track