useActionState EverywhereuseActionState has been around for a while now. The world worked before it existed, and it continues to work just fine with or without it.
Recently, someone on my team claimed that “the React way” is to use useActionState for all form and state-management needs. That’s the question I want to tackle here:
Should you reach for useActionState everywhere?
Spoiler: No.
Not because the hook is bad—it’s intentionally minimal and well-designed—but because it solves a very specific problem and does very little beyond that.
useActionState Actually IsThe API:
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?)
It takes:
fn)And returns:
React’s own description is very clear:
“useActionState is specifically for updating client component state from form submissions (usually from Server Actions).”
This is not a general-purpose state hook. This is not a replacement for React Query or SWR. This is not a magic input-state manager.
It’s a tiny glue layer for connecting HTML <form> submissions to state updates in client components.
And it does that well.
"use client";
import { useActionState } from "react";
type OutputType = string[];
const INITIAL_STATE: OutputType = [];
export default function Page() {
const [state, formAction, isPending] = useActionState(operation, INITIAL_STATE);
return (
<>
<h2>Input Form</h2>
<form action={formAction}>
<input type="text" name="query" />
<button type="submit" disabled={isPending}>
Submit
</button>
</form>
<h2>Output</h2>
<pre>{isPending ? "Loading..." : JSON.stringify(state, null, 2)}</pre>
</>
);
}
async function operation(initialOutput: OutputType, input: FormData) {
await new Promise((resolve) => setTimeout(resolve, 2000));
return ["result1", "result2", "result3"];
}
For a simple form, this is perfect:
If your entire app were made of forms like this, useActionState would be a dream.
But most apps aren’t.
One of the features of useActionState—and also one of its limitations—is that it deliberately ignores input state.
React’s direction with RSC and Actions is:
Let the browser own form state. Don’t mirror it in JS unless absolutely necessary.
That’s great for progressive enhancement and simple forms.
But modern forms aren’t always simple:
If your form logic involves anything beyond static HTML, you end up managing input state manually anyway. And because useActionState only gives you FormData, you still need to extract and interpret everything yourself.
This isn’t wrong. It’s just not a full form solution.
useActionState only updates state through a form submission.
Great when:
But sometimes:
For example, a search page:
User bookmarks: /search?q=cats
They return to the page
No results show
Because the form hasn’t been submitted yet
So you either:
All of these are workarounds around the hook's very narrow update path.
Again, not React’s fault—useActionState is doing its job. But if your workflow requires multiple ways of updating state, this hook becomes a straightjacket.
If fn throws, the error propagates like any other async error in React:
useActionState does not give you:
error fieldstatus fieldIf you want error state, you’ll need to:
Totally doable. But it’s manual.
If you have more than a couple of forms, this becomes boilerplate quickly.
If you try to treat useActionState as a generic “form handler,” you end up writing the same glue logic repeatedly:
When a primitive hook doesn’t solve a broad problem space, the complexity doesn’t vanish—it just migrates into userland. Across every form.
That’s the waterbed theory of complexity: Push down here → it pops up over there.
useActionState for what it excels atThis is where it shines.
Libraries like React Query, SWR, or your own custom hooks give you:
If your business logic is non-trivial, these tools align better with your needs.
If you're repeating the same pattern—search forms, multi-step forms, data-entry screens—create:
SearchForm componentFormWithErrors componentValidatedForm componentWizardForm componentThese can encapsulate your workflow and reduce boilerplate dramatically.
Opinionated? Sure. But it keeps the complexity in one place, not sprinkled across all your pages and components.
useActionState is a small, elegant tool for a small, specific job:
updating state from HTML form submissions.
It’s not a general-purpose form handler. It’s not a state management library. It’s not meant to replace client-side logic for complex forms.
Use it where it makes sense: simple, linear form flows with minimal state.
For everything else, either build your own abstractions or use libraries designed for richer workflows.
It’s not that useActionState is bad.
It’s that using it everywhere leads to more complexity, not less.