Skip to the solution
HardFrontend7 min read

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

1.1k views

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.

  • react
  • react-19
  • actions
  • forms

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

7 min read Β· 10 sections

Try it in the playground

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

ReactThe same form written the old way and as an Action
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>
  );
}

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 73 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 nextHard

What is `useFormStatus` in React 19, and why does it exist?

Continue