useActionState

Wraps an action so its return value becomes state and exposes a pending flag while it runs.

Signature
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);
  • Added in React 19.0.0

Parameters

ParameterTypeDescription
$reducerAction(previousState: S, payload: P) => S | Promise<S>The function called when the Action is triggered. It receives the previous state as its first argument — initialState on the first call, then its own previous return value — followed by the payload passed to dispatchAction. It may be async and may perform side effects.
$initialStateSThe value the state should be initially. React ignores this argument after dispatchAction is invoked for the first time.
$permalinkoptionalstringA unique page URL that this form modifies. Used with Server Components for progressive enhancement: if reducerAction is a Server Function and the form is submitted before the JavaScript bundle loads, the browser navigates to this URL instead of the current page's.

Return value

[state: S, dispatchAction: (payload: P) => void, isPending: boolean]An array with exactly three values: the current state (initialState on the first render, then whatever reducerAction returned); the dispatchAction function to call inside an Action; and the isPending flag, true while any dispatched Action for this Hook is running.

Errors

If the action throws, React cancels every queued Action for the Hook and the error surfaces at the nearest error boundary.

Changelog

  • PHP 19.0.0Replaces useFormState from react-dom, which took the same first two arguments but returned only two values.
  • PHP 19.0.0Adds isPending as a third return value, so a separate useTransition is no longer needed to disable the submit button.

useActionState turns "submit something, wait, show the result" into one Hook. You give it a function; its return value becomes state, and isPending is true while it runs. Passing the returned dispatch function to a form's action prop wires the whole submit path in one line.

When to reach for it

  • A form whose response is validation errors or a success message you need to render.
  • Any submit button that must disable itself while the request is in flight.
  • Forms that should work before hydration, when the action is a Server Function.

What trips people up

The action's first parameter is the previous state, not the form data. The payload — FormData when dispatched by a form — is the second argument. Writing async function submit(formData) is the single most common mistake and gives you the previous state where you expected fields.

The dispatch function must be called from an Action: a <form action>, an Action prop, or inside startTransition. A bare onClick calling it directly will not work.

Also note that whatever the action returns replaces the whole state. Return the fields you still need, including the submitted values, or the form resets to empty on an error.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Submit a signup form and render field-level validation errors from the response.
  • Disable a Save button and show a spinner while the update is in flight.
  • Keep a running result across submissions, such as a retry counter or last saved timestamp.
  • Give a Server Function form a permalink so it still works before JavaScript loads.

Frequently asked questions

Why is formData undefined in my action?

Because the first parameter is the previous state, not the form data. Declare the action as async (previousState, formData) => { ... }; the payload is always the second argument. When dispatched from a form action prop that payload is a FormData object, and when dispatched manually it is whatever value you passed.

What replaced useFormState in React 19?

useActionState from react, which takes the same reducer-style action and initial state but returns three values instead of two — the extra isPending flag removes the need for a separate useTransition. Update the import from react-dom to react and destructure the third element; useFormStatus, which reads the status of a parent form from a child, is unrelated and still lives in react-dom.

Can I use useActionState without a form or a Server Function?

Yes. The action can be any async client function and the dispatch can be called from anywhere, as long as the call happens inside an Action — wrap it in startTransition when there is no form action prop to do it for you. Server Functions and the permalink argument are optional extras for progressive enhancement.

More examples

Dispatching outside a form with startTransition

The payload here is a number, not FormData — dispatchAction passes through whatever you give it as the action's second argument. Rapid clicks queue rather than race: React runs the actions sequentially, and each one sees the previous return value as previousState, so the score never loses a vote.

Example · jsx
import { useActionState, startTransition } from "react";

async function vote(previousState, delta) {
  await new Promise((resolve) => setTimeout(resolve, 400));
  return { score: previousState.score + delta, lastDelta: delta };
}

export default function VoteWidget() {
  const [state, dispatchVote, isPending] = useActionState(vote, {
    score: 0,
    lastDelta: 0,
  });

  // Not a form: the dispatch still has to run inside an Action.
  function cast(delta) {
    startTransition(() => dispatchVote(delta));
  }

  return (
    <div>
      <button onClick={() => cast(1)} disabled={isPending}>
        Upvote
      </button>{" "}
      <button onClick={() => cast(-1)} disabled={isPending}>
        Downvote
      </button>
      <p>
        score: {state.score} (last change {state.lastDelta})
      </p>
    </div>
  );
}

A Server Function with a permalink

Shows the progressive-enhancement path: the form posts and re-renders with a message even if the JavaScript bundle has not loaded, in which case the browser lands on /newsletter. Note that subscribe returns email in both branches — the state replaces the previous one wholesale, so dropping that field would blank the input after a failed submit.

Example · jsx
"use client";

import { useActionState } from "react";
import { subscribe } from "./actions";

const INITIAL = { message: null, ok: false, email: "" };

export default function SubscribeForm() {
  const [state, formAction, isPending] = useActionState(
    subscribe,
    INITIAL,
    // Before hydration, a submit navigates here instead of the current URL.
    "/newsletter"
  );

  return (
    <form action={formAction}>
      <input
        name="email"
        type="email"
        defaultValue={state.email}
        required
      />
      <button disabled={isPending}>
        {isPending ? "Subscribing…" : "Subscribe"}
      </button>
      {state.message ? (
        <p role={state.ok ? "status" : "alert"}>{state.message}</p>
      ) : null}
    </form>
  );
}

// ./actions.js
// "use server";
//
// export async function subscribe(previousState, formData) {
//   const email = String(formData.get("email") ?? "");
//   const existing = await db.subscriber.findUnique({ where: { email } });
//   if (existing) {
//     return { message: "That address is already subscribed.", ok: false, email };
//   }
//   await db.subscriber.create({ data: { email } });
//   return { message: "Check your inbox to confirm.", ok: true, email: "" };
// }

Discussion

  • Be the first to comment on this lesson.