DEV Community

Vikrant Bagal
Vikrant Bagal

Posted on

React 19 useActionState: Practical Examples That Replace Your Old Form Code

React 19 useActionState: Practical Examples That Replace Your Old Form Code

React 19 introduced useActionState, a hook that fundamentally changes how you handle form submissions and async actions. If you're still managing loading states, error states, and submitted values with separate useState calls, this hook will cut your boilerplate in half.

What useActionState Does

useActionState wraps an async action function and returns three values:

  • state — whatever your action returns
  • dispatch — a function to trigger the action
  • isPendingtrue while the action is in flight
const [state, dispatch, isPending] = useActionState(
  async (prevState, formData) => {
    // Your async logic here
    return { success: true, message: "Done" };
  },
  initialState
);
Enter fullscreen mode Exit fullscreen mode

This replaces the React 18 pattern of juggling useState for loading, errors, and results — plus manual e.preventDefault() and try/catch blocks.

Example 1: Simple Form with Validation

Here's a common pattern: a form that validates input, shows loading state, and displays success or error messages.

import { useActionState } from "react";

async function submitForm(prevState, formData) {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1500));

  const email = formData.get("email");
  if (!email || !email.includes("@")) {
    return { success: false, message: "Please enter a valid email." };
  }

  return { success: true, message: "Submitted successfully!" };
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {
    success: null,
    message: "",
  });

  return (
    <form action={formAction}>
      <input type="text" name="name" placeholder="Name" />
      <input type="email" name="email" placeholder="Email" />
      <button disabled={isPending}>
        {isPending ? "Submitting..." : "Submit"}
      </button>

      {state.message && (
        <p className={state.success ? "success" : "error"}>
          {state.message}
        </p>
      )}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key observations:

  • No useState for isPending, error, or success — all handled by the hook
  • No e.preventDefault() — the action attribute handles it
  • The action receives formData directly via the FormData API
  • Validation and error handling happen inside the action function

Example 2: Counter with Async Increment

For non-form actions, you can call the dispatch function imperatively with startTransition:

import { useActionState, startTransition } from "react";
import { addToCart } from "./api";

function CartButton() {
  const [count, dispatch, isPending] = useActionState(
    async (prevCount) => {
      return await addToCart(prevCount);
    },
    0
  );

  function handleClick() {
    startTransition(() => {
      dispatch();
    });
  }

  return (
    <div>
      <span>Items: {count}</span>
      <button onClick={handleClick} disabled={isPending}>
        Add to Cart{isPending ? " 🌀" : ""}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This pattern is useful for buttons that trigger side effects without a form — like adding items to a cart, liking a post, or following a user.

Example 3: Multiple Independent Actions in One Component

You can use multiple useActionState calls in the same component, each managing its own state:

import { useActionState } from "react";
import { toggleLike, toggleFollow } from "./actions";

function SocialActions() {
  const [liked, likeAction] = useActionState(toggleLike, false);
  const [following, followAction] = useActionState(toggleFollow, false);

  return (
    <div>
      <form action={likeAction}>
        <button>{liked ? "❤️ Liked" : "♡ Like"}</button>
      </form>
      <form action={followAction}>
        <button>{following ? "✔ Following" : "+ Follow"}</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Each action is completely independent — toggling "like" doesn't affect the "follow" state. This is much cleaner than managing multiple useState pairs with separate loading flags.

Example 4: With useFormStatus for Child Components

If you need pending state in a child component (like a loading spinner inside a button), use useFormStatus — but it must be in a child of the form, not the form itself:

import { useActionState } from "react";
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Saving..." : "Save"}
    </button>
  );
}

function ProfileForm() {
  const [state, formAction] = useActionState(
    async (prev, formData) => {
      await saveProfile(formData);
      return { saved: true };
    },
    { saved: false }
  );

  return (
    <form action={formAction}>
      <input name="name" defaultValue="John" />
      <SubmitButton /> {/* useFormStatus works here */}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example 5: Error-Only State Pattern

For simple cases where you only care about errors (not success state), use a string as the initial state:

import { useActionState } from "react";

function NameForm() {
  const [error, submitAction, isPending] = useActionState(
    async (prev, formData) => {
      const response = await fetch("/api/update-name", {
        method: "POST",
        body: JSON.stringify({ name: formData.get("name") }),
      });

      if (!response.ok) {
        const { message } = await response.json();
        return message; // Return error string
      }
      return ""; // Empty string = no error
    },
    "" // Initial state: no error
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Save</button>
      {isPending && <Spinner />}
      {error && <p className="error">{error}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

React 18 vs React 19: The Difference

React 18 (old way):

const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
const [result, setResult] = useState(null);

async function handleSubmit(e) {
  e.preventDefault();
  setIsPending(true);
  setError(null);
  try {
    const res = await api.submit(formData);
    setResult(res);
  } catch (err) {
    setError(err.message);
  } finally {
    setIsPending(false);
  }
}

<form onSubmit={handleSubmit}>
Enter fullscreen mode Exit fullscreen mode

React 19 (new way):

const [state, formAction, isPending] = useActionState(
  async (prev, formData) => {
    try {
      const res = await api.submit(formData);
      return { success: true, data: res };
    } catch (err) {
      return { success: false, error: err.message };
    }
  },
  { success: null, data: null, error: null }
);

<form action={formAction}>
Enter fullscreen mode Exit fullscreen mode

The React 19 version eliminates:

  • Three separate useState calls
  • Manual e.preventDefault()
  • Manual setIsPending(true/false)
  • Try/catch/finally boilerplate

When to Use useActionState

  • Form submissions — any form that sends data to a server
  • Async actions — like/dislike, follow/unfollow, add to cart
  • Optimistic updates — combine with useOptimistic for instant UI feedback
  • Server Functions — works seamlessly with React Server Components

When NOT to Use It

  • Simple synchronous state updates (just use useState)
  • Complex multi-step wizards (consider a state machine or form library)
  • When you need fine-grained control over every state transition

Key Takeaways

  1. useActionState consolidates loading, error, and result state into one hook
  2. Works with both form actions (<form action={formAction}>) and imperative calls (dispatch())
  3. The action function receives formData via the standard FormData API
  4. Combine with useFormStatus in child components for pending state
  5. Multiple useActionState calls in one component are independent

The hook is stable in React 19 and ready for production. If you're still writing React 18-style form handlers, it's time to simplify.


Sources:

What's your experience with useActionState? Are you still on React 18 form patterns, or have you made the switch?

Top comments (0)