Skip to solution
hardFrontend

What are Actions in React 19, and how do they change form handling?

1.1k views
01

Understand the problem

Question presented to candidate: "React 19 lets you pass a function to a form's action prop. What does that actually buy you over an onSubmit handler?"

What a strong answer should cover:

  • An Action is a function React runs on your behalf — passed to <form action>, or to startTransition — for which React manages the pending state, errors, and optimistic updates itself.
  • Passing a function to action gives you the FormData directly. No preventDefault, no reading refs, no state per field.
  • React resets an uncontrolled form after the action resolves successfully, which is the behaviour you would otherwise write by hand.
  • The submission runs inside a transition, so the UI stays responsive and isPending is something React knows rather than something you track.
  • The practical consequence: uncontrolled forms are the default again. Most fields need no useState at all.
  • The surrounding hooks complete the picture: useActionState for the result and pending flag, useFormStatus for a nested submit button, useOptimistic for instant feedback.
  • With a framework, an action can be a Server Action and the form works before JavaScript loads — genuine progressive enhancement.

Clarifying questions expected:

  • "Is this a plain client app or a framework with Server Actions?" — progressive enhancement only applies to the second.
  • "Do any fields need per-keystroke validation?" — those still want controlled inputs.

Code / implementation expected: Optional. Contrasting a <form action={fn}> against the onSubmit version it replaces is the clearest form.

reactreact-19actionsforms
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 forms and hooks basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every behaviour below was executed against React 19.2.8 by rendering a f

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same form written the old way and as an Action
Run Playground
import { useState, useRef } from "react";

const save = (name) =>
  new Promise((r) => setTimeout(() => r("saved " + JSON.stringify(name)), 500));

// ❌ THE OLD WAY — state per field, a submitting flag, preventDefault, and a
//    manual reset. Every line here is boilerplate React now handles.
function OldWay({ onResult }) {
  const [name, setName] = useState("");
  const [submitting, setSubmitting] = useState(false);

  const onSubmit = async (e) => {
    e.preventDefault();
    setSubmitting(true);
    try {
      onResult("old: " + (await save(name)));
      setName("");                       // manual reset
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <form onSubmit={onSubmit} style={box}>
      <strong style={{ fontSize: 13 }}>❌ onSubmit</strong>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="name"
        style={{ width: "100%", margin: "6px 0" }}
      />
      <button type="submit" disabled={submitting}>
        {submitting ? "saving…" : "save"}
      </button>
    </form>
  );
}

// ✅ AS AN ACTION — the input is uncontrolled, the values arrive as FormData,
//    and React resets the form when the action resolves.
function ActionWay({ onResult }) {
  return (
    <form
      action={async (formData) => {
        onResult("action: " + (await save(formData.get("name"))));
      }}
      style={box}
    >
      <strong style={{ fontSize: 13 }}>✅ action</strong>
      <input name="name" placeholder="name" style={{ width: "100%", margin: "6px 0" }} />
      <button type="submit">save</button>
    </form>
  );
}

const box = { border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 };

export default function App() {
  const [log, setLog] = useState([]);
  const push = (s) => setLog((l) => [s, ...l].slice(0, 5));

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <OldWay onResult={push} />
      <ActionWay onResult={push} />

      <pre style={{ background: "#f6f6f8", padding: 10, borderRadius: 8, fontSize: 12, minHeight: 70 }}>
{log.length ? log.join("\n") : "submit either form"}
      </pre>

      <p style={{ fontSize: 13, color: "#666" }}>
        Type into both and submit. They behave the same — but the second has no
        state, no preventDefault and no reset code. Note the action version
        clears itself when the promise resolves: React resets an uncontrolled
        form after a successful action, exactly like a real HTML submission.
      </p>
    </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 73 of 119 decoded in the React.js track. One more won't hurt.

Back to track