Nahid Akbar

Why Not Use React’s useActionState Everywhere

useActionState 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.


What useActionState Actually Is

The API:

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?)

It takes:

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.


Minimum Example

"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.


Problem 1: No Input State Management

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.


Problem 2: Only One Trigger Path (Submit the Form)

useActionState only updates state through a form submission.

Great when:

But sometimes:

For example, a search page:

  1. User bookmarks: /search?q=cats

  2. They return to the page

  3. No results show

  4. Because the form hasn’t been submitted yet

  5. So you either:

    • artificially trigger a form submit, or
    • call the action manually (which breaks the model), or
    • lift the logic into a parent component, or
    • fetch everything server-side and bypass the hook entirely

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.


Problem 3: No Built-In Error Handling

If fn throws, the error propagates like any other async error in React:

useActionState does not give you:

If 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.


Problem 4: Boilerplate Everywhere

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.


So What Should You Do Instead?

Option 1: Use useActionState for what it excels at

This is where it shines.

Option 2: For anything complex, build abstractions or use a library

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.

Option 3: Build reusable components, not just hooks

If you're repeating the same pattern—search forms, multi-step forms, data-entry screens—create:

These 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.


Conclusion

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.

Written November 2025
© Nahid Akbar