`useActionState` for Pending State and Server Errors

How to use the useActionState hook with a Server Action to show pending state and surface validation errors returned from the server.

7 min read

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.

index.tsindex.ts
// 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.

App.tsxApp.tsx
'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.

ValueWhat it holds
stateThe current state, starting at the initial value you passed
formActionThe dispatcher to pass to the form action prop
isPendingTrue 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.

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

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.
RunePowered by Rune AI

Frequently Asked Questions

What does useActionState return?

An array of three values: the current state, the dispatchAction function to pass to the form, and an isPending boolean that is true while the action runs.

Why does my action receive the previous state as the first argument?

The reducerAction passed to useActionState receives the previous state first, then the submitted payload. With a form, FormData is the second argument, not the first.

Should I return errors or throw them?

Return known validation errors as part of the state so the form can display them. Throw only unexpected errors, which React routes to the nearest error boundary.

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.