Skip to content
React rc forms 4 min read

Form Actions & useActionState

React 19 introduces a modern, declarative model for handling form submissions called Actions. Instead of wiring up an onSubmit handler, calling preventDefault(), and juggling useState flags for pending and error states, you pass a function directly to a form’s action prop. React then manages the submission lifecycle for you and exposes purpose-built hooks — useActionState and useFormStatus — to read pending status and results. This approach reduces boilerplate, works with progressive enhancement, and integrates cleanly with server-side mutations.

The form action prop

In React 19, the <form> element accepts a function for its action prop. When the form is submitted, React calls that function with a FormData object containing the form fields. React automatically prevents the default browser submission and resets the form on success.

function NewsletterForm() {
  async function subscribe(formData) {
    const email = formData.get("email");
    await fetch("/api/subscribe", {
      method: "POST",
      body: JSON.stringify({ email }),
      headers: { "Content-Type": "application/json" },
    });
  }

  return (
    <form action={subscribe}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

Because the action receives FormData, you read values by name rather than tracking each input with controlled state. This keeps simple forms uncontrolled and lean.

Action functions can be synchronous or asynchronous. If the function returns a promise, React treats the form as pending until it resolves, which powers the hooks below.

Tracking state with useActionState

useActionState wraps an action so you can read the latest result and a pending flag. It is the cornerstone hook for forms that need to show validation messages, success states, or errors.

The hook signature is useActionState(actionFn, initialState). It returns a tuple: the current state, a wrapped action to pass to <form action>, and an isPending boolean.

import { useActionState } from "react";

async function login(previousState, formData) {
  const email = formData.get("email");
  const password = formData.get("password");

  const res = await fetch("/api/login", {
    method: "POST",
    body: JSON.stringify({ email, password }),
    headers: { "Content-Type": "application/json" },
  });

  if (!res.ok) {
    return { error: "Invalid credentials", email };
  }
  return { success: true };
}

function LoginForm() {
  const [state, formAction, isPending] = useActionState(login, {});

  return (
    <form action={formAction}>
      <input name="email" type="email" defaultValue={state.email} required />
      <input name="password" type="password" required />
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="ok">Welcome back!</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? "Signing in…" : "Sign in"}
      </button>
    </form>
  );
}

Note that the action’s first argument is the previous state and the second is the FormData. The value you return becomes the next state. Returning the submitted email lets you repopulate the field after a failed attempt — a clean way to preserve user input.

Nested submit buttons with useFormStatus

When a submit button lives in a separate component (a shared <SubmitButton>, for example), it does not have access to the parent’s isPending value. The useFormStatus hook solves this: it reads the status of the nearest parent form without prop drilling.

import { useFormStatus } from "react-dom";

function SubmitButton({ children }) {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting…" : children}
    </button>
  );
}

function ContactForm({ action }) {
  return (
    <form action={action}>
      <textarea name="message" required />
      <SubmitButton>Send message</SubmitButton>
    </form>
  );
}

useFormStatus is imported from react-dom, not react, and must be called from a component rendered inside a <form>. Calling it in the same component that renders the <form> returns pending: false.

The hook returns several fields:

FieldTypeDescription
pendingbooleanWhether the parent form is currently submitting
dataFormData | nullThe data being submitted
methodstring | nullThe HTTP method (get or post)
actionfunction | string | nullThe action handler or URL

Progressive enhancement and server actions

Because actions are built on native form semantics, they degrade gracefully. If you pass a string URL to action, the form performs a standard browser POST when JavaScript has not yet loaded, then upgrades to the client-side action once React hydrates.

In React Server Components frameworks (such as Next.js with the App Router), an action can be a Server Action marked with the "use server" directive. The function runs on the server, so you can query a database directly inside it.

"use server";

import { db } from "@/lib/db";

export async function createTodo(previousState, formData) {
  const title = formData.get("title");
  if (!title) return { error: "Title is required" };

  await db.todo.create({ data: { title } });
  return { success: true };
}

This server action can be passed to useActionState in a client component exactly like a local function, giving you the same pending and result handling while the mutation runs securely on the server.

Best practices

  • Prefer the action prop over manual onSubmit handlers for new forms — let React manage pending and reset behavior.
  • Read field values from the FormData argument instead of mirroring every input in controlled state.
  • Use useActionState when you need to surface errors, success messages, or repopulate fields after a failed submission.
  • Reach for useFormStatus to build reusable submit buttons and spinners that stay in sync with their parent form.
  • Always disable the submit button while pending to prevent duplicate submissions.
  • Pass a string URL or a Server Action to keep forms functional before hydration for genuine progressive enhancement.
  • Remember useFormStatus comes from react-dom and only works inside a descendant of the <form>.
Last updated June 14, 2026
Was this helpful?