useActionState()
Runs an action through a reducer and returns its result, a dispatch function, and a pending flag.
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?)- Added in Next.js v15.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$reducerAction | (previousState: State, payload: Payload) => State | Promise<State> | 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. |
$initialState | State | The value the state holds before the first dispatch. React ignores it after dispatchAction runs once. It must be serializable when reducerAction is a Server Function. |
$permalinkoptional | string | The unique page URL this form modifies. If the Server Function is submitted before the JavaScript bundle loads, the browser navigates to this URL rather than the current one. |
Return value
[state: State, dispatchAction: (payload: Payload) => void, isPending: boolean]An array of three values: the current state (initialState on first render, then whatever reducerAction last returned), a dispatch function to call from inside an Action, and a boolean that is true while any dispatched Action for this hook is pending.
Errors
If the dispatched action throws, React cancels every queued action for the hook and surfaces the error at the nearest error boundary.
Changelog
- PHP v15.0.0Replaces useFormState from react-dom, which is imported from react and adds the isPending flag as a third return value.
useActionState is how a form gets an answer back from its action. Instead of the action returning into nowhere, its return value becomes the hook's state, which you render as an error message, a success banner, or the new row. The third slot gives you a pending boolean without a second hook.
When to reach for it
- Server-side validation that has to be shown next to the field that failed.
- Any submission where the user needs to see the outcome without a navigation.
- A sequence of dispatches that must accumulate — React queues them and hands each call the previous result.
What trips people up
The action's signature changes when it moves into this hook. A function written as createUser(formData) must become createUser(prevState, formData); forget it and formData.get is called on the state object. The dispatch function must run inside an Action — pass it to <form action>, or wrap the call in startTransition, otherwise React logs an error in development and the update is not tracked as a transition. With a Server Function, initialState is serialized, so keep it to plain objects and primitives.
Example
When to use it
- Show a Zod validation error under the email input after a failed signup.
- Disable the submit button and swap its label while the action is in flight.
- Accumulate the result of repeated dispatches, such as an incrementing quantity.
Frequently asked questions
What is the difference between useActionState and useFormStatus?
useActionState owns the action and gives you its return value plus a pending flag in the component that renders the form; useFormStatus only reads pending status and must be called from a child component inside the form. Use the first when you need the result, the second when a nested button just needs to disable itself.
Why is formData.get is not a function inside my action?
The action is receiving the previous state as its first argument, so `formData` has shifted to the second parameter. Change the signature to `(prevState, formData)` — useActionState always prepends the state.
Does useActionState work without JavaScript?
The form still submits and the action still runs, because `<form action>` degrades to a native POST. Pass the `permalink` argument so the browser lands on the right URL if the bundle has not loaded yet.
More examples
A signup form with server-side validation
The action returns a plain object rather than throwing, so a failed validation re-renders the form with the message in place instead of hitting the error boundary. `pending` covers the whole round trip, including the server re-render that ships back with the action's response.
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({ email: z.string().email('Invalid email') })
export type SignupState = { message: string; errors?: { email?: string[] } }
export async function createUser(
prevState: SignupState,
formData: FormData
): Promise<SignupState> {
const parsed = schema.safeParse({ email: formData.get('email') })
if (!parsed.success) {
return { message: '', errors: parsed.error.flatten().fieldErrors }
}
await db.user.create({ data: { email: parsed.data.email } })
return { message: 'Check your inbox to confirm.' }
}
// app/ui/signup.tsx
'use client'
import { useActionState } from 'react'
import { createUser, type SignupState } from '@/app/actions'
const initialState: SignupState = { message: '' }
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
<p aria-live="polite">{state.errors?.email?.[0] ?? state.message}</p>
<button disabled={pending}>Sign up</button>
</form>
)
}Dispatching outside a form
Outside a `<form action>`, the dispatch has to be wrapped in `startTransition` so React scopes it as an Action. The commented-out line is the same call without the wrapper: it still fires the action, but the pending flag never flips and development logs an error.
'use client'
import { useActionState, startTransition } from 'react'
import { vote } from '@/app/actions'
export function VoteButton({ postId }: { postId: string }) {
const [votes, dispatch, isPending] = useActionState(vote, 0)
return (
<button
// Wrong: React logs "An async function was passed to useActionState..."
// onClick={() => dispatch(postId)}
onClick={() => startTransition(() => dispatch(postId))}
disabled={isPending}
>
{votes} {isPending ? 'saving…' : 'votes'}
</button>
)
}Migrating from useFormState
The import moves from `react-dom` to `react`, the name changes, and a third `isPending` value appears — which usually lets you delete a separate `useFormStatus` submit button. The action itself needs no edit, because both hooks pass the previous state as the first argument.
// Before — React 18 / Next.js 14
import { useFormState } from 'react-dom'
const [state, formAction] = useFormState(createUser, initialState)
// After — React 19 / Next.js 15+
import { useActionState } from 'react'
const [state, formAction, isPending] = useActionState(createUser, initialState)
// The action signature is unchanged by the migration:
// (prevState, formData) => nextState
Discussion