useActionState Hook
Manage form state driven by an action function, with built-in pending state and progressive enhancement.
Search across all documentation pages
Manage form state driven by an action function, with built-in pending state and progressive enhancement.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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).
"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 errorFormData, returning the next stateisPending disables the button and shows loading text during submissionuseActionState wraps your action function and returns a form-compatible action, the current state, and a pending flagFormDataisPending becomes true without blocking the UIisPending to falseformAction returned is compatible with the <form action={}> pattern, enabling progressive enhancement| Parameter | Type | Description |
|---|---|---|
action | (prevState: S, formData: FormData) => S or Promise<S> | Function called on form submission |
initialState | S | Initial state before any submission |
permalink | string (optional) | URL for progressive enhancement (server components) |
| Return | Type | Description |
|---|---|---|
state | S | Current state (updated after each action completes) |
formAction | (formData: FormData) => void | Action to pass to <form action={}> or <button formAction={}> |
isPending | boolean | true while the action is running |
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: "" };
}// 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: ActionStateConfusing 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 form - useActionState 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.
| Alternative | Use When | Don't Use When |
|---|---|---|
useState + useTransition | Custom submit logic not tied to <form action={}> | You want progressive enhancement |
useReducer | Complex client-side state transitions without form submission | State changes are driven by form actions |
| React Hook Form | Complex validation, field-level errors, dynamic forms | Simple forms with server actions |
| Server action without hook | Fire-and-forget mutation, no client state update needed | You 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.
useFormState to useActionState and added isPending as the third return value.useActionState from "react", not useFormState from "react-dom".formAction returned by the hook is compatible with <form action={}>.return { ...prevState, error: "" }.undefined after submission, which may break your UI.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: "" });isPending becomes true without blocking the UI.isPending to false.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: ActionStateDate objects, Map, and Set will fail during serialization.<form>
<input name="item" />
<button formAction={saveDraftAction}>Save Draft</button>
<button formAction={publishAction}>Publish</button>
</form>formAction attribute pointing to a different action.useActionState for form submissions where you want progressive enhancement and automatic pending state.useState + useTransition for custom submit logic not tied to <form action={}>.useActionState reduces boilerplate by combining state, pending, and action into one hook.useActionState uses transitions internallyReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥