State, Action, Pending — From One Hook
useActionState — Read Action Result on the Client
React's `useActionState` (formerly `useFormState`) lets a Client Component read what an action returned — perfect for showing validation errors next to the form.
What you'll learn
- Wrap an action with `useActionState`
- Render returned errors and messages
- Use the pending state for disabled buttons
A bare <form action> calls the action and revalidates — but it has no way to surface
“email is invalid” back to the user. useActionState gives the action a return value
that the client can render.
The Hook Signature
const [state, formAction, pending] = useActionState(action, initialState).
The action becomes formAction. Its first argument is the previous state; its second is
the FormData.
Wiring It Up
// actions.ts
'use server'
import { revalidatePath } from 'next/cache'
type State = { error: string | null }
export async function createNote(prev: State, form: FormData): Promise<State> {
const title = String(form.get('title') ?? '')
if (title.length < 3) return { error: 'Title must be at least 3 characters' }
await db.notes.insert({ title })
revalidatePath('/notes')
return { error: null }
} // NoteForm.tsx
'use client'
import { useActionState } from 'react'
import { createNote } from './actions'
export default function NoteForm() {
const [state, formAction, pending] = useActionState(createNote, { error: null })
return (
<form action={formAction}>
<input name="title" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>
</form>
)
} The error text renders only on a failed submission. Pending state powers the button label.
Why This Beats Plain useState
Pending tracking is handled by React, including during the revalidation phase after a successful action. You do not have to wire up your own loading flags or worry about race conditions between submits.
useFormStatus →