Hook useActionState
Gestiona el estado del formulario impulsado por una función de acción, con estado de pendiente incorporado y mejora progresiva.
Receta
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).
Ejemplo funcional
"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:
useActionStategestiona todo el ciclo de vida del formulario: inactivo, pendiente, éxito y error- La acción recibe el estado anterior y
FormData, devolviendo el estado siguiente isPendingdesactiva el botón y muestra texto de carga durante el envío- El formulario funciona con mejora progresiva — puede enviarse incluso antes de que JavaScript se cargue
Análisis profundo
Cómo funciona
useActionStateenvuelve tu función de acción y devuelve una acción compatible con formularios, el estado actual y una bandera de pendiente- Cuando el formulario se envía, React llama a tu acción con el estado anterior y
FormData - La acción se ejecuta dentro de una transición, por lo que
isPendingse convierte entruesin bloquear la UI - Después de que la acción se resuelve, React actualiza el estado con el valor devuelto y establece
isPendingenfalse - El
formActiondevuelto es compatible con el patrón<form action={}>, permitiendo mejora progresiva - Si se usa con acciones del servidor en Next.js, el formulario funciona antes de que JavaScript se hidrate
Parámetros y valores de retorno
| 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 |
Variaciones
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: "" };
}Notas sobre TypeScript
// 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: ActionStateTrampas
-
Confundido con useFormState (obsoleto) — React 19 renombró
useFormStateauseActionStatey agregóisPendingcomo el tercer valor de retorno. Solución: UsauseActionStatede"react", nouseFormStatede"react-dom". -
La acción debe devolver estado — Si tu acción no devuelve un valor, el estado se convierte en
undefineddespué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 —
useActionStateestá diseñado para<form action={}>. Llamar manualmente aformActionconFormDataconstruido 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.
Alternativas
| 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.
FAQs
¿Cuál es la diferencia entre useActionState y el useFormState obsoleto?
- React 19 renombró
useFormStateauseActionStatey agregóisPendingcomo el tercer valor de retorno. - Importa
useActionStatede"react", nouseFormStatede"react-dom". - De lo contrario, la API es la misma: función de acción, estado inicial y permalink opcional.
¿Cómo proporciona useActionState mejora progresiva?
- El
formActiondevuelto por el hook es compatible con<form action={}>. - Esto significa que el formulario puede enviarse incluso antes de que JavaScript se hidrate en la página.
- Con acciones del servidor en Next.js, el formulario funciona sin ningún JavaScript del lado del cliente.
¿Por qué la función de acción recibe previousState como su primer argumento?
- Permite actualizaciones incrementales de estado, similar a un patrón reductor.
- Puedes fusionar el estado anterior con nuevos datos:
return { ...prevState, error: "" }. - Sin ella, perderías campos de estado existentes en cada envío.
Trampa: ¿Qué sucede si mi función de acción no devuelve un valor?
- El estado se convierte en
undefineddespués del envío, lo que puede romper tu UI. - Siempre devuelve el objeto de nuevo estado desde la función de acción.
- TypeScript te advertirá si el tipo de retorno no coincide con el tipo de estado.
¿Puedo usar useActionState para formularios solo de cliente sin acciones del servidor?
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: "" });- Sí. La acción puede ser cualquier función asincrónica, no solo una acción del servidor.
¿Cómo funciona isPending internamente?
- La acción se ejecuta dentro de una transición de React, por lo que
isPendingse convierte entruesin bloquear la UI. - Después de que la acción se resuelve, React actualiza el estado y establece
isPendingenfalse. - No necesitas gestionar manualmente el estado de carga.
¿Cómo tipificar la acción y el estado con TypeScript?
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: ActionStateTrampa: ¿Puedo pasar valores no serializables (funciones, Dates, Maps) en el estado?
- No. Al usar acciones del servidor, el estado se serializa entre servidor y cliente.
- Solo objetos simples con valores primitivos (strings, números, booleanos, arrays) son seguros.
- Funciones, objetos
Date,MapySetfallarán durante la serialización.
¿Cómo manejo múltiples botones de envío con diferentes acciones?
<form>
<input name="item" />
<button formAction={saveDraftAction}>Guardar borrador</button>
<button formAction={publishAction}>Publicar</button>
</form>- Cada botón puede tener su propio atributo
formActionapuntando a una acción diferente.
¿Cuándo debo usar useActionState vs useState + useTransition manualmente?
- Usa
useActionStatepara envíos de formulario donde deseas mejora progresiva y estado de pendiente automático. - Usa
useState+useTransitionpara lógica de envío personalizada no vinculada a<form action={}>. useActionStatereduce el código repetitivo al combinar estado, pendiente y acción en un hook.
Relacionado
- useOptimistic — mostrar estado optimista mientras la acción está pendiente
- useTransition —
useActionStateusa transiciones internamente - use — primitivo de React 19 para consumir promises
- useReducer — patrón similar pero para máquinas de estado del lado del cliente