React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-19formsactionsserver-actionshooks

useActionState Hook

Manage form state driven by an action function, with built-in pending state and progressive enhancement.

Recipe

Quick-reference recipe card — copy-paste ready.

const [state, formAction, isPending] = useActionState(action, initialState);
 
// action signature
async function action(previousState: State, formData: FormData): Promise<State> {
  // process form data, return new state
}
 
// Use in a form
<form action={formAction}>
  <input name="email" />
  <button disabled={isPending}>Submit</button>
  {state.error && <p>{state.error}</p>}
</form>

When to reach for this: You have a form that submits data (to a server action or async function) and you want React to manage the submission state, pending indicator, and result — with progressive enhancement (works without JavaScript).

Working Example

"use client";
 
import { useActionState } from "react";
 
interface FormState {
  message: string;
  error: string;
}
 
async function submitFeedback(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const feedback = formData.get("feedback") as string;
 
  if (!feedback || feedback.trim().length < 10) {
    return { message: "", error: "Feedback must be at least 10 characters." };
  }
 
  // Simulate server delay
  await new Promise((resolve) => setTimeout(resolve, 1000));
 
  return { message: `Thanks for your feedback!`, error: "" };
}
 
const initialState: FormState = { message: "", error: "" };
 
export function FeedbackForm() {
  const [state, formAction, isPending] = useActionState(submitFeedback, initialState);
 
  return (
    <form action={formAction} className="space-y-3 max-w-sm">
      <label className="block">
        <span className="text-sm font-medium">Your Feedback</span>
        <textarea
          name="feedback"
          rows={3}
          className="mt-1 block w-full border rounded px-3 py-2"
          required
        />
      </label>
 
      <button
        type="submit"
        disabled={isPending}
        className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {isPending ? "Submitting..." : "Submit"}
      </button>
 
      {state.error && <p className="text-sm text-red-600">{state.error}</p>}
      {state.message && <p className="text-sm text-green-600">{state.message}</p>}
    </form>
  );
}

What this demonstrates:

  • useActionState manages the entire form lifecycle: idle, pending, success, and error
  • The action receives the previous state and FormData, returning the next state
  • isPending disables the button and shows loading text during submission
  • The form works with progressive enhancement — it can submit even before JavaScript loads

Deep Dive

How It Works

  • useActionState wraps your action function and returns a form-compatible action, the current state, and a pending flag
  • When the form submits, React calls your action with the previous state and the FormData
  • The action runs inside a transition, so isPending becomes true without blocking the UI
  • After the action resolves, React updates the state with the returned value and sets isPending to false
  • The formAction returned is compatible with the <form action={}> pattern, enabling progressive enhancement
  • If used with server actions in Next.js, the form works before JavaScript hydrates

Parameters & Return Values

ParameterTypeDescription
action(prevState: S, formData: FormData) => S or Promise<S>Function called on form submission
initialStateSInitial state before any submission
permalinkstring (optional)URL for progressive enhancement (server components)
ReturnTypeDescription
stateSCurrent state (updated after each action completes)
formAction(formData: FormData) => voidAction to pass to <form action={}> or <button formAction={}>
isPendingbooleantrue while the action is running

Variations

With server action (Next.js App Router):

// app/actions.ts
"use server";
 
export async function createUser(prevState: FormState, formData: FormData) {
  const name = formData.get("name") as string;
  const user = await db.users.create({ data: { name } });
  return { success: true, error: "" };
}
 
// app/page.tsx
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
 
export function CreateUserForm() {
  const [state, formAction, isPending] = useActionState(createUser, {
    success: false,
    error: "",
  });
  return <form action={formAction}>...</form>;
}

Multiple submit buttons with formAction:

<form>
  <input name="item" />
  <button formAction={saveAction}>Save Draft</button>
  <button formAction={publishAction}>Publish</button>
</form>

Client-only async action:

async function loginAction(prev: LoginState, formData: FormData) {
  const res = await fetch("/api/login", {
    method: "POST",
    body: formData,
  });
  if (!res.ok) return { error: "Invalid credentials" };
  return { error: "" };
}

TypeScript Notes

// Type the state explicitly for clarity
interface ActionState {
  success: boolean;
  error: string;
  data?: UserData;
}
 
// The action must match the state type
async function myAction(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  // ...
  return { success: true, error: "" };
}
 
const [state, formAction, isPending] = useActionState(myAction, {
  success: false,
  error: "",
});
// state: ActionState

Gotchas

  • Confusing with useFormState (deprecated) — React 19 renamed useFormState to useActionState and added isPending as the third return value. Fix: Use useActionState from "react", not useFormState from "react-dom".

  • Action must return state — If your action doesn't return a value, state becomes undefined after submission. Fix: Always return the new state from your action function.

  • State resets on each submission — The previous state is passed as the first argument; you must merge it if you want to preserve fields. Fix: Spread previous state: return { ...prevState, error: "" }.

  • Using outside a formuseActionState is designed for <form action={}>. Calling formAction manually with constructed FormData works but loses progressive enhancement. Fix: Prefer <form action={formAction}> for best compatibility.

  • Server action serialization — State passed between server and client must be serializable (no functions, Dates, Maps). Fix: Use plain objects with primitive values.

Alternatives

AlternativeUse WhenDon't Use When
useState + useTransitionCustom submit logic not tied to <form action={}>You want progressive enhancement
useReducerComplex client-side state transitions without form submissionState changes are driven by form actions
React Hook FormComplex validation, field-level errors, dynamic formsSimple forms with server actions
Server action without hookFire-and-forget mutation, no client state update neededYou need to display the result in the UI

Why useActionState over manual fetch? useActionState gives you pending state, error handling, and progressive enhancement in one hook — no need to wire up useState + useTransition + try/catch manually.

FAQs

What is the difference between useActionState and the deprecated useFormState?
  • React 19 renamed useFormState to useActionState and added isPending as the third return value.
  • Import useActionState from "react", not useFormState from "react-dom".
  • The API is otherwise the same: action function, initial state, and optional permalink.
How does useActionState provide progressive enhancement?
  • The formAction returned by the hook is compatible with <form action={}>.
  • This means the form can submit even before JavaScript hydrates on the page.
  • With server actions in Next.js, the form works without any client-side JavaScript.
Why does the action function receive previousState as its first argument?
  • It allows incremental state updates, similar to a reducer pattern.
  • You can merge the previous state with new data: return { ...prevState, error: "" }.
  • Without it, you would lose existing state fields on each submission.
Gotcha: What happens if my action function doesn't return a value?
  • The state becomes undefined after submission, which may break your UI.
  • Always return the new state object from the action function.
  • TypeScript will warn you if the return type doesn't match the state type.
Can I use useActionState for client-only forms without server actions?
async function loginAction(prev: LoginState, formData: FormData) {
  const res = await fetch("/api/login", {
    method: "POST",
    body: formData,
  });
  if (!res.ok) return { error: "Invalid credentials" };
  return { error: "" };
}
 
const [state, formAction, isPending] = useActionState(loginAction, { error: "" });
  • Yes. The action can be any async function, not just a server action.
How does isPending work internally?
  • The action runs inside a React transition, so isPending becomes true without blocking the UI.
  • After the action resolves, React updates the state and sets isPending to false.
  • You don't need to manage loading state manually.
How do I type the action and state with TypeScript?
interface ActionState {
  success: boolean;
  error: string;
}
 
async function myAction(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  return { success: true, error: "" };
}
 
const [state, formAction, isPending] = useActionState(myAction, {
  success: false,
  error: "",
});
// state: ActionState
Gotcha: Can I pass non-serializable values (functions, Dates, Maps) in the state?
  • No. When using server actions, state is serialized between server and client.
  • Only plain objects with primitive values (strings, numbers, booleans, arrays) are safe.
  • Functions, Date objects, Map, and Set will fail during serialization.
How do I handle multiple submit buttons with different actions?
<form>
  <input name="item" />
  <button formAction={saveDraftAction}>Save Draft</button>
  <button formAction={publishAction}>Publish</button>
</form>
  • Each button can have its own formAction attribute pointing to a different action.
When should I use useActionState vs useState + useTransition manually?
  • Use useActionState for form submissions where you want progressive enhancement and automatic pending state.
  • Use useState + useTransition for custom submit logic not tied to <form action={}>.
  • useActionState reduces boilerplate by combining state, pending, and action into one hook.
  • useOptimistic — show optimistic state while the action is pending
  • useTransitionuseActionState uses transitions internally
  • use — React 19 primitive for consuming promises
  • useReducer — similar pattern but for client-side state machines