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:
- No loading feedback
- Double submissions
- Manual error state management
useActionState solves all this by:
- Automatic
pendingtracking during async operations - Latest action result (success/error/data) returned directly
- Built-in double-submit prevention
- Perfect Server Actions companion
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
| Parameter | Type | Description |
|---|---|---|
action | Function | Server Action or async function |
initialState | any | Starting state (defaults to undefined) |
| Return Value | Type | Description |
|---|---|---|
state | any | Latest action result |
dispatch | Function | Form action handler |
isPending | boolean | True during action execution |
When to Reach for It
β
Use useActionState when:
- Building forms with Server Actions
- Need loading states + error handling
- Working with Next.js App Router
- Replacing
useFormStatus+ manual state
β Skip it when:
- Simple client-side state
- No async operations
- Legacy
pages/router
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?