Skip to content
Go back

Mastering useActionState in React 19 - The Future of Form Handling

Mastering useActionState in React 19: Smarter Form Handling with Server Actions

React 19 introduces useActionState, a game-changing hook that simplifies managing form states tied to server actions. Say goodbye to juggling useState + useTransitionβ€”this hook gives you a dispatch function and built-in pending state in one clean package.

Why useActionState Changes Everything

Traditional forms create poor UX:

useActionState solves all this by:

Pro Tip: Ideal for Next.js apps, validation-heavy forms, or any async user action.

πŸš€ Basic Usage: Login Form Example

// app/actions.js (Server Action)
"use server";

export async function loginAction(prevState, formData) {
  const email = formData.get("email");
  const password = formData.get("password");

  // Simulate API/auth check
  if (email === "test@example.com" && password === "password") {
    return { success: true, message: "Welcome back!" };
  }

  return { success: false, message: "Invalid credentials" };
}
// Login component
"use client";

import { useActionState } from "react";
import { loginAction } from "./actions";

export default function LoginForm() {
  const [state, submitAction, isPending] = useActionState(loginAction, null);

  return (
    <form
      action={submitAction}
      className="mx-auto max-w-md space-y-4 rounded-xl bg-white p-8 shadow-lg"
    >
      <div>
        <label className="mb-2 block text-sm font-medium">Email</label>
        <input
          type="email"
          name="email"
          className="w-full rounded-lg border border-gray-300 p-3 focus:ring-2 focus:ring-blue-500"
          placeholder="test@example.com"
          disabled={isPending}
          required
        />
      </div>

      <div>
        <label className="mb-2 block text-sm font-medium">Password</label>
        <input
          type="password"
          name="password"
          className="w-full rounded-lg border border-gray-300 p-3 focus:ring-2 focus:ring-blue-500"
          placeholder="- - - - - - - - "
          disabled={isPending}
          required
        />
      </div>

      <button
        type="submit"
        disabled={isPending}
        className="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 px-4 py-3 font-medium text-white transition-all duration-200 hover:from-blue-600 hover:to-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
      >
        {isPending ? (
          <>
            <svg className="h-5 w-5 animate-spin" viewBox="0 0 24 24">
              <circle
                cx="12"
                cy="12"
                r="10"
                stroke="currentColor"
                strokeWidth="4"
                fill="none"
                strokeLinecap="round"
              />
            </svg>
            Signing in...
          </>
        ) : (
          "Sign In"
        )}
      </button>

      {state?.message && (
        <div
          className={`rounded-lg p-4 text-sm ${
            state.success
              ? "border border-green-200 bg-green-50 text-green-800"
              : "border border-red-200 bg-red-50 text-red-800"
          }`}
        >
          {state.message}
        </div>
      )}
    </form>
  );
}

πŸ”„ Advanced: Todo App with Optimistic Updates

Handle prevState for complex flows like todos:

// app/actions.js
"use server";

export async function addTodo(prevState, formData) {
  const text = formData.get("todo")?.toString().trim();

  if (!text || text.length < 3) {
    return {
      error: "Todo must be 3+ characters",
      todos: prevState?.todos || [],
    };
  }

  // Simulate DB insert
  const newTodo = { id: Date.now(), text, completed: false };

  return {
    success: true,
    newTodo,
    todos: [...(prevState?.todos || []), newTodo],
  };
}
function TodoForm({ todos }) {
  const [state, addTodoAction, isPending] = useActionState(addTodo, { todos });

  return (
    <div className="space-y-4">
      <form action={addTodoAction} className="flex gap-2">
        <input
          name="todo"
          placeholder="Add a new todo..."
          className="flex-1 rounded-lg border p-3 focus:ring-2 focus:ring-blue-500"
          disabled={isPending}
        />
        <button
          type="submit"
          disabled={isPending}
          className="rounded-lg bg-green-500 px-6 py-3 text-white hover:bg-green-600 disabled:opacity-50"
        >
          {isPending ? "Adding..." : "Add"}
        </button>
      </form>

      {state?.error && (
        <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-red-800">
          {state.error}
        </div>
      )}

      {state?.newTodo && (
        <div className="animate-in slide-in-from-bottom-2 rounded-lg border border-blue-200 bg-blue-50 p-3 text-blue-800 duration-300">
          βœ… Added: <strong>{state.newTodo.text}</strong>
        </div>
      )}
    </div>
  );
}

useActionState API Reference

ParameterTypeDescription
actionFunctionServer Action or async function
initialStateanyStarting state (defaults to undefined)
Return ValueTypeDescription
stateanyLatest action result
dispatchFunctionForm action handler
isPendingbooleanTrue during action execution

When to Reach for It

βœ… Use useActionState when:

❌ Skip it when:

The Future is Here

useActionState + Server Actions = full-stack React without the boilerplate. Your forms become instantly more polished and predictable.

Try it: React 19 Docs

What React 19 hook are you most excited about?


Share this post on:

Previous Post
My AI-Powered Project Planning Workflow: From Chaos to Code-Ready Architecture
Next Post
Mastering the JavaScript Event Loop: A Practical Guide for Beginners