useActionState is a React hook that gives a form a state object, an action dispatcher, and a pending flag in one call. Pass it a Server Action and it surfaces the action's return value plus any validation error the server sends back, all while JavaScript is still loading. Think of it as useReducer for form submissions, where the reducer may run on the server and return its result.
The Server Action takes two parameters. useActionState hands it the previous state first, then the submitted FormData, so the signature is not the plain FormData one you use with a bare form action.
// app/actions.ts
'use server'
type State = { error?: string; title?: string } | null
export async function createPost(prevState: State, formData: FormData) {
const title = formData.get('title')?.toString()
if (!title) return { error: 'Title is required.' }
return { title }
}The action ignores prevState here and reads the title from the second parameter. Returning an object is what puts the message on screen, because that return value becomes the next state.
Now wire it into a form with useActionState, in a component file such as app/create-post.tsx. It must be a Client Component, because hooks like useActionState can only run in the browser. The directive keeps the component client-side while the action it imports stays on the server.
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'
export function CreatePost() {
const [state, formAction, isPending] = useActionState(createPost, null)
return <form action={formAction}>
<label>Title <input name="title" required /></label>
<button disabled={isPending}>Save</button>
{state?.error && <p role="alert">{state.error}</p>}
</form>
}When the form submits, the browser sends the fields as FormData and createPost runs on the server. The returned object becomes state, so the error message appears without any extra fetch. The button needs no type attribute because a button inside a form submits by default.
The three values
useActionState returns an array of three values that cover the whole submission lifecycle. The state can be any serializable value, so you are free to model it as a string, a boolean, or an object with an error field.
| Value | What it holds |
|---|---|
| state | The current state, starting at the initial value you passed |
| formAction | The dispatcher to pass to the form action prop |
| isPending | True while the action is in flight |
The dispatcher is what you give to the form. React wraps the submission in a transition automatically, so you do not need startTransition for the common form case. That automatic transition is also why isPending updates without extra wiring, unlike a bare event handler where you wrap the call yourself.
Returning server errors as state
The action returns an object, and that object becomes state on the next render. Returning { error: 'Title is required.' } is the known-error path: the form reads state.error and shows it in an element with role alert.
For an unexpected failure, throw instead. React cancels queued actions and sends the error to the nearest error boundary. Return errors you expect, throw errors you cannot recover from.
A thrown error is for cases where continuing would be wrong, such as a broken invariant. A returned error is for cases the visitor can fix, such as a missing field. See server-side form validation with Zod for returning field-level errors at scale.
Showing pending state
The third value, isPending, flips true while the action runs and back to false when it settles.
Disabling the submit button while it is true is the main use. Without that, a double click queues two submissions, and because actions dispatch one at a time the second sits waiting on the first. Swapping the label to Saving or rendering a spinner works from the same flag.
Forgetting the argument order is the usual first bug here. An action written to take FormData alone receives the previous state in that slot instead, so every field reads back as undefined. See how to mutate data with a Server Action for the full mutation pattern, and optimistic UI with useOptimistic when you want the UI to update before the server responds.
Progressive enhancement with permalink
useActionState accepts an optional third argument, a permalink string naming the page the form modifies.
It only matters before hydration. If someone submits the form while the JavaScript bundle is still downloading, React has no client-side action to run, so it falls back to a normal browser submission and navigates to that permalink.
In practice most Next.js apps never pass it. The framework already handles the pre-hydration path for Server Action forms, so reach for permalink only when you have a form whose target page differs from the one it renders on, such as a search box that posts to a results route.
Rune AI
Key Insights
- useActionState returns state, dispatchAction, and isPending.
- Pass the dispatchAction to the form action prop.
- The Server Action receives previous state first, then FormData.
- Return known errors as state and show them in the UI.
- isPending lets you disable the submit button while the action runs.
Frequently Asked Questions
What does useActionState return?
Why does my action receive the previous state as the first argument?
Should I return errors or throw them?
Conclusion
useActionState gives a form one place for its state, its action dispatcher, and a pending flag. Pass a Server Action to it and return validation errors as state, so the UI shows pending and server messages without extra wiring.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.