Hook useActionState
Gestiona el estado del formulario impulsado por una función de acción, con estado de pendiente incorporado y mejora progresiva.
Busca en todas las páginas de la documentación
Gestiona el estado del formulario impulsado por una función de acción, con estado de pendiente incorporado y mejora progresiva.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
const [state, formAction, isPending] = useActionState(action, initialState);
// firma de la acción
async function action(previousState: State, formData: FormData): Promise<State> {
// procesa los datos del formulario, devuelve el nuevo estado
}
// Usar en un formulario
<form action={formAction}>
<input name="email" />
<button disabled={isPending}>Enviar</button>
{state.error && <p>{state.error}</p>}
</form>Cuándo usarlo: Tienes un formulario que envía datos (a una acción del servidor o función asincrónica) y deseas que React gestione el estado del envío, el indicador de pendiente y el resultado - con mejora progresiva (funciona sin JavaScript).
"use client";
import { useActionState } from "react";
interface FormState {
message: string;
error: string;
}
async function submitFeedback(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const feedback = formData.get("feedback") as string;
if (!feedback || feedback.trim().length < 10) {
return { message: "", error: "El comentario debe tener al menos 10 caracteres." };
}
// Simula un retraso en el servidor
await new Promise((resolve) => setTimeout(resolve, 1000));
return { message: `¡Gracias por tu comentario!`, error: "" };
}
const initialState: FormState = { message: "", error: "" };
export function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, initialState);
return (
<form action={formAction} className="space-y-3 max-w-sm">
<label className="block">
<span className="text-sm font-medium">Tu comentario</span>
<textarea
name="feedback"
rows={3}
className="mt-1 block w-full border rounded px-3 py-2"
required
/>
</label>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isPending ? "Enviando..." : "Enviar"}
</button>
{state.error && <p className="text-sm text-red-600">{state.error}</p>}
{state.message && <p className="text-sm text-green-600">{state.message}</p>}
</form>
);
}Lo que esto demuestra:
useActionState gestiona todo el ciclo de vida del formulario: inactivo, pendiente, éxito y errorFormData, devolviendo el estado siguienteisPending desactiva el botón y muestra texto de carga durante el envíouseActionState envuelve tu función de acción y devuelve una acción compatible con formularios, el estado actual y una bandera de pendienteFormDataisPending se convierte en true sin bloquear la UIisPending en falseformAction devuelto es compatible con el patrón <form action={}>, permitiendo mejora progresiva| Parámetro | Tipo | Descripción |
|---|---|---|
action | (prevState: S, formData: FormData) => S o Promise<S> | Función llamada en el envío del formulario |
initialState | S | Estado inicial antes de cualquier envío |
permalink | string (opcional) | URL para mejora progresiva (componentes del servidor) |
| Retorno | Tipo | Descripción |
|---|---|---|
state | S | Estado actual (actualizado después de que cada acción se completa) |
formAction | (formData: FormData) => void | Acción para pasar a <form action={}> o <button formAction={}> |
isPending | boolean | true mientras la acción se está ejecutando |
Con acción del servidor (Next.js App Router):
// app/actions.ts
"use server";
export async function createUser(prevState: FormState, formData: FormData) {
const name = formData.get("name") as string;
const user = await db.users.create({ data: { name } });
return { success: true, error: "" };
}
// app/page.tsx
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(createUser, {
success: false,
error: "",
});
return <form action={formAction}>...</form>;
}Múltiples botones de envío con formAction:
<form>
<input name="item" />
<button formAction={saveAction}>Guardar borrador</button>
<button formAction={publishAction}>Publicar</button>
</form>Acción asincrónica solo para cliente:
async function loginAction(prev: LoginState, formData: FormData) {
const res = await fetch("/api/login", {
method: "POST",
body: formData,
});
if (!res.ok) return { error: "Credenciales inválidas" };
return { error: "" };
}// Tipifica explícitamente el estado para mayor claridad
interface ActionState {
success: boolean;
error: string;
data?: UserData;
}
// La acción debe coincidir con el tipo de estado
async function myAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
// ...
return { success: true, error: "" };
}
const [state, formAction, isPending] = useActionState(myAction, {
success: false,
error: "",
});
// state: ActionStateConfundido con useFormState (obsoleto) - React 19 renombró useFormState a useActionState y agregó isPending como el tercer valor de retorno. Solución: Usa useActionState de "react", no useFormState de "react-dom".
La acción debe devolver estado - Si tu acción no devuelve un valor, el estado se convierte en undefined después del envío. Solución: Siempre devuelve el nuevo estado desde tu función de acción.
El estado se reinicia en cada envío - El estado anterior se pasa como el primer argumento; debes fusionarlo si deseas preservar campos. Solución: Expande el estado anterior: return { ...prevState, error: "" }.
Usar fuera de un formulario - useActionState está diseñado para <form action={}>. Llamar manualmente a formAction con FormData construido funciona pero pierde mejora progresiva. Solución: Prefiere <form action={formAction}> para la mejor compatibilidad.
Serialización de acción del servidor - El estado pasado entre servidor y cliente debe ser serializable (sin funciones, Dates, Maps). Solución: Usa objetos simples con valores primitivos.
| Alternativa | Usa cuando | No uses cuando |
|---|---|---|
useState + useTransition | Lógica de envío personalizada no vinculada a <form action={}> | Deseas mejora progresiva |
useReducer | Transiciones de estado complejas en el lado del cliente sin envío de formulario | Los cambios de estado están impulsados por acciones de formulario |
| React Hook Form | Validación compleja, errores a nivel de campo, formularios dinámicos | Formularios simples con acciones del servidor |
| Acción del servidor sin hook | Mutación de "disparar y olvidar", sin actualización de estado de cliente | Necesitas mostrar el resultado en la UI |
¿Por qué useActionState en lugar de fetch manual? useActionState te proporciona estado de pendiente, manejo de errores y mejora progresiva en un hook - sin necesidad de conectar manualmente useState + useTransition + try/catch.
useFormState a useActionState y agregó isPending como el tercer valor de retorno.useActionState de "react", no useFormState de "react-dom".formAction devuelto por el hook es compatible con <form action={}>.return { ...prevState, error: "" }.undefined después del envío, lo que puede romper tu UI.async function loginAction(prev: LoginState, formData: FormData) {
const res = await fetch("/api/login", {
method: "POST",
body: formData,
});
if (!res.ok) return { error: "Credenciales inválidas" };
return { error: "" };
}
const [state, formAction, isPending] = useActionState(loginAction, { error: "" });isPending se convierte en true sin bloquear la UI.isPending en false.interface ActionState {
success: boolean;
error: string;
}
async function myAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
return { success: true, error: "" };
}
const [state, formAction, isPending] = useActionState(myAction, {
success: false,
error: "",
});
// state: ActionStateDate, Map y Set fallarán durante la serialización.<form>
<input name="item" />
<button formAction={saveDraftAction}>Guardar borrador</button>
<button formAction={publishAction}>Publicar</button>
</form>formAction apuntando a una acción diferente.useActionState para envíos de formulario donde deseas mejora progresiva y estado de pendiente automático.useState + useTransition para lógica de envío personalizada no vinculada a <form action={}>.useActionState reduce el código repetitivo al combinar estado, pendiente y acción en un hook.useActionState usa transiciones internamenteRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥