The useActionState hook
By Flavio Copes
Learn how the React useActionState hook lets a client component read a server action's state and pending status to show messages and disable the button.
The useActionState hook lets a client component read the state returned by a server action, and know when the action is running. It’s the built-in way to show server feedback in a form.
(in previous React canary versions it was known as useFormState)
Why does this hook exist?
Before it, wiring a form to a server action left you with two manual jobs: getting the action’s return value back into the component, and tracking a loading flag yourself.
Client components can use this hook to “peek” into the state of a server action, and both problems go away.
How do you use it?
We pass it the server action, and an initial state object.
What we get back is the state object, the form action we attach to the form, and a pending boolean:
"use client"
import { useActionState } from "react"
import { myServerAction } from './actions'
const initialState = {
message: "",
}
export const Demo = () => {
const [state, formAction, pending] =
useActionState(myServerAction, initialState)
return (
<div>
<form action={formAction}>
<input
type='text'
name='fullName'
/>
{state?.message && <p>{state.message}</p>}
<button
aria-disabled={pending}
type='submit'>
{pending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
)
}
On the first render, state is the initial state we passed. After each submit, state becomes whatever the action returned. In this case we assume it returns an object with a message string.
While the action runs, pending is true, so we can disable the button and change its label. When the action finishes, pending goes back to false and the message shows up.
Stuff we didn’t implement before, but you can see how quick it is with useActionState.
The action signature changes
Here’s the pitfall that catches everyone. When an action is wrapped by useActionState, React calls it with the previous state as the first argument, and the form data as the second:
"use server"
export async function myServerAction(prevState, formData) {
const fullName = formData.get('fullName')
if (!fullName) {
return { message: 'Please enter your name' }
}
//save it somewhere...
return { message: `Thanks, ${fullName}!` }
}
If your action was written as myServerAction(formData), it breaks the moment you pass it to the hook, because formData now arrives in the second position. You’d be calling .get() on the previous state and wondering why it fails.
The fix is adding the prevState parameter, even if you never use it.