Skip to content

React Tutorial Forms and Validation

React 19 Form Actions: action, useActionState and useFormStatus

Lesson 81 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

React 19 added a new way to handle forms: you pass a function to the form’s action prop. React calls it with the form data and takes care of the common chores for you: no preventDefault, automatic pending state and automatic reset of uncontrolled fields.

The action prop

App.jsx
JSX
function App() {
  const [message, setMessage] = React.useState("");

  function subscribe(formData) {
    const email = formData.get("email");
    setMessage("Subscribed: " + email);
  }

  return (
    <form action={subscribe}>
      <input name="email" type="email" aria-label="Email" required />
      <button type="submit">Subscribe</button>
      <p role="status">{message}</p>
    </form>
  );
}

Compare with a classic handler:

onSubmitaction
Receives an event; you call preventDefault()Receives a FormData; React prevents the reload
You read values from state or FormDataValues come from the form’s named inputs
Fields keep their text unless you reset themUncontrolled fields reset automatically after the action
You track pending state yourselfThe action can be async, and React tracks pending state

Inputs need a name because the values come from FormData.

useActionState: result and pending state

useActionState wraps an action and gives you the latest result, a wrapped action to pass to the form, and an isPending flag:

App.jsx
JSX
const [state, formAction, isPending] = useActionState(actionFn, initialState);

actionFn(previousState, formData) returns the next state.

App.jsx
JSX
function App() {
  const [state, formAction, isPending] = React.useActionState(async (prev, formData) => {
    const name = formData.get("name");
    await new Promise((r) => setTimeout(r, 700)); // pretend to call a server
    if (!name) return { ok: false, message: "Please enter a name." };
    return { ok: true, message: "Welcome, " + name + "!" };
  }, { ok: null, message: "" });

  return (
    <form action={formAction}>
      <h3>Join the list</h3>
      <input name="name" aria-label="Name" />
      <button type="submit" disabled={isPending}>{isPending ? "Joining..." : "Join"}</button>
      <p role="status" style={{ color: state.ok === false ? "#b71c1c" : "inherit" }}>{state.message}</p>
    </form>
  );
}

While the async action runs, isPending is true, so you can disable the button. When it finishes, state holds whatever the action returned. In your own project this is where a server call goes.

useFormStatus: pending state in child components

useFormStatus (imported from react-dom) lets a component inside a form read the form’s status without props. It is ideal for a reusable submit button:

App.jsx
JSX
import { useFormStatus } from "react-dom";

function SubmitButton({ children }) {
  const { pending } = useFormStatus();   // must be rendered INSIDE a <form>
  return <button type="submit" disabled={pending}>{pending ? "Saving..." : children}</button>;
}

function App() {
  const [saved, setSaved] = React.useState(false);

  async function save() {
    await new Promise((r) => setTimeout(r, 600));
    setSaved(true);
  }

  return (
    <form action={save}>
      <p>Save profile</p>
      <SubmitButton>Save</SubmitButton>
      {saved && <p>Saved</p>}
    </form>
  );
}

useFormStatus reads the parent form, so calling it in the same component that renders the <form> does not work; it needs a child component, as above.

useOptimistic: show the result first

useOptimistic shows the expected result immediately while the real request is in progress, and replaces it with the real state when done (or rolls back on failure). It is useful for likes, comments and todo lists. See the React documentation for the full API.

Actions are not only for forms

Any function passed to startTransition can be an Action: React tracks its pending state and keeps the UI responsive. Form action props are the most visible use.

When to choose which style

  • onSubmit + controlled state: you need live validation, formatting or changing fields from code.
  • action + FormData: a simple submit with fields you only need on submit, especially with servers and frameworks that run actions on the server (such as a React framework with Server Functions).
  • A form library: many fields with dependent rules.

In a client-only Vite app the action runs in the browser, exactly like the examples here. Frameworks can run the same pattern on the server, which is why the API looks the way it does.

Key points

  • Pass a function to <form action={fn}>; it receives FormData and uncontrolled fields reset automatically.
  • useActionState returns the result, a form action and a pending flag.
  • useFormStatus reads the pending state from a child inside a form.