Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
async function subscribe(_prev: string, formData: FormData): Promise<string> {
const email = formData.get("email") as string;
const res = await fetch("/api/subscribe", {
method: "POST",
body: JSON.stringify({ email }),
});
if (!res.ok) return "Algo salió mal.";
return "¡Suscrito!";
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Suscribiendo..." : "Suscribirse"}
</button>
);
}
export default function NewsletterForm() {
const [message, formAction, isPending] = useActionState(subscribe, "");
return (
<form action={formAction}>
<input name="email" type="email" required placeholder="you@example.com" />
<SubmitButton />
{message && <p>{message}</p>}
</form>
);
}Cuándo usarlo: Siempre que tengas un formulario que envíe datos -- registros, búsquedas, operaciones CRUD. Las acciones de formulario reemplazan el patrón manual onSubmit + preventDefault + useState.
// Un formulario de contacto con validación, visualización de errores y mejora progresiva
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
type FormState = {
success: boolean;
errors: Record<string, string>;
message: string;
};
const initialState: FormState = {
success: false,
errors: {},
message: "",
};
async function submitContact(_prev: FormState, formData: FormData): Promise<FormState> {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const body = formData.get("body") as string;
// Validación del lado del cliente
const errors: Record<string, string> = {};
if (!name || name.length < 2) errors.name = "El nombre debe tener al menos 2 caracteres.";
if (!email || !email.includes("@")) errors.email = "Por favor ingresa un email válido.";
if (!body || body.length < 10) errors.body = "El mensaje debe tener al menos 10 caracteres.";
if (Object.keys(errors).length > 0) {
return { success: false, errors, message: "Por favor corrige los errores abajo." };
}
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, body }),
});
if (!res.ok) throw new Error("Error del servidor");
return { success: true, errors: {}, message: "¡Mensaje enviado! Nos pondremos en contacto." };
} catch {
return { success: false, errors: {}, message: "Falló el envío. Por favor intenta de nuevo." };
}
}
function FieldError({ error }: { error?: string }) {
if (!error) return null;
return <p className="text-red-500 text-sm mt-1">{error}</p>;
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{pending ? "Enviando..." : "Enviar Mensaje"}
</button>
);
}
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, initialState);
return (
<form action={formAction} className="space-y-4 max-w-md">
<div>
<label htmlFor="name">Nombre</label>
<input id="name" name="name" className="border w-full p-2 rounded" />
<FieldError error={state.errors.name} />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" className="border w-full p-2 rounded" />
<FieldError error={state.errors.email} />
</div>
<div>
<label htmlFor="body">Mensaje</label>
<textarea id="body" name="body" rows={4} className="border w-full p-2 rounded" />
<FieldError error={state.errors.body} />
</div>
<SubmitButton />
{state.message && (
<p className={state.success ? "text-green-600" : "text-red-600"}>
{state.message}
</p>
)}
</form>
);
}Lo que esto demuestra:
useActionState manejando state de formulario a través de envíos (errores, mensaje de éxito)useFormStatus en un componente hijo para mostrar el estado pending en el botón de envío<form action={fn}> -- React 19 extiende el atributo action nativo para aceptar funciones async. Cuando se envía el formulario, React llama a fn(formData) y gestiona el ciclo de vida pending.useActionState(action, initialState, permalink?) -- Envuelve una función action y devuelve [state, wrappedAction, isPending]. Cada vez que se envía el formulario, se llama a action(prevState, formData) y el valor devuelto se convierte en el nuevo state. El parámetro opcional permalink habilita la mejora progresiva para server actions.useFormStatus() -- Debe ser llamado desde un componente renderizado dentro de un <form>. Devuelve { pending, data, method, action } reflejando el estado de envío del formulario padre más cercano.useActionState, los formularios pueden funcionar antes de que se cargue JavaScript. El argumento permalink especifica dónde redirigir después de que se complete la server action en el caso sin JS.isPending de useActionState refleja si la action se está ejecutando actualmente. Esto está disponible en el mismo componente que llama a useActionState, a diferencia de useFormStatus que solo funciona en descendientes.Múltiples botones de envío con diferentes acciones:
"use client";
import { useActionState } from "react";
export default function ItemForm() {
const [saveResult, saveAction] = useActionState(saveItem, null);
const [deleteResult, deleteAction] = useActionState(deleteItem, null);
return (
<form>
<input name="name" />
<button formAction={saveAction}>Guardar</button>
<button formAction={deleteAction}>Eliminar</button>
</form>
);
}Usando useFormStatus para un indicador de carga global:
"use client";
import { useFormStatus } from "react-dom";
export function FormProgress() {
const { pending, data } = useFormStatus();
if (!pending) return null;
return (
<div className="fixed top-0 left-0 w-full h-1 bg-blue-500 animate-pulse" />
);
}
// Usa dentro de cualquier formulario
<form action={myAction}>
<FormProgress />
{/* ... fields ... */}
</form>Reiniciar formulario después de éxito:
"use client";
import { useActionState, useRef, useEffect } from "react";
export default function ResetableForm() {
const [state, formAction, isPending] = useActionState(submitData, { success: false });
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (state.success) {
formRef.current?.reset();
}
}, [state]);
return (
<form ref={formRef} action={formAction}>
<input name="item" required />
<button type="submit" disabled={isPending}>Agregar</button>
</form>
);
}useActionState es genérica: useActionState<State>(fn: (prev: State, formData: FormData) => Promise<State>, initial: State).useFormStatus devuelve { pending: boolean; data: FormData | null; method: string; action: string | ((formData: FormData) => void) | null }.(previousState: State, formData: FormData) => State | Promise<State>.permalink en useActionState se tipea como string | undefined.useFormStatus debe estar dentro de un descendiente de <form> -- Llamarla en el mismo componente que renderiza la etiqueta <form> devuelve { pending: false } siempre. Solución: Extrae el botón de envío en un componente hijo.return { ...prev, error: "..." }.useRef en el formulario y llama a formRef.current?.reset() en un effect.useActionState -- Se importa desde "react", no desde "react-dom". El antiguo useFormState estaba en "react-dom" y ahora está deprecated. Solución: import { useActionState } from "react".formData.useActionState es independiente. Enviar un formulario no afecta a otro. Solución: Este es el comportamiento esperado pero ten en cuenta que useFormStatus solo refleja el formulario ancestro más cercano.| Enfoque | Cuándo elegir |
|---|---|
| Form Actions + useActionState | Formularios React 19 estándar con pending state integrado |
| react-hook-form | Validación compleja del lado del cliente, arrays de campos, comportamiento watch |
| Formik | Proyectos legacy que ya usan Formik |
| Manual onSubmit + fetch | Control total sobre el ciclo de vida de la solicitud, headers personalizados |
| Server action only (sin client state) | Mutaciones simples que redirigen después de completarse |
| Conform | Mejora progresiva con integración Zod para server actions |
<form action={fn}> permite que React gestione el pending state, manejo de errores y actualizaciones optimistas automáticamenteonSubmit + preventDefault + useState requiere gestión manual de state[state, wrappedAction, isPending]state se actualiza cada vez que se completa la action, basado en lo que devuelve la actionwrappedAction se pasa a <form action={}> e isPending indica si la action se está ejecutando actualmenteuseFormStatus se importa desde "react-dom", mientras que useActionState se importa desde "react"useFormStatus debe ser llamado desde un componente renderizado dentro de un <form> (un descendiente)isPending de useActionState está disponible en el mismo componente que renderiza el formularioDevuelve un objeto errors desde la action y renderízalo por campo:
async function submit(_prev: State, formData: FormData) {
const errors: Record<string, string> = {};
if (!formData.get("name")) errors.name = "Requerido";
if (Object.keys(errors).length) return { errors };
// ... guardar datos
return { errors: {} };
}const formRef = useRef<HTMLFormElement>(null);
const [state, formAction] = useActionState(submit, initial);
useEffect(() => {
if (state.success) formRef.current?.reset();
}, [state]);
<form ref={formRef} action={formAction}>...</form>Las acciones de formulario de React no reinician el formulario automáticamente a diferencia de los envíos de formulario nativos.
Sí. Usa el atributo formAction en botones individuales:
<form>
<input name="name" />
<button formAction={saveAction}>Guardar</button>
<button formAction={deleteAction}>Eliminar</button>
</form>useFormStatus debe ser llamado desde un descendiente del elemento <form>, no del mismo componente que lo renderizauseFormStatusreturn { ...prev, error: "..." }useActionState<State>(
fn: (prev: State, formData: FormData) => Promise<State>,
initial: State
): [State, (formData: FormData) => void, boolean]La firma de la función action es (previousState: State, formData: FormData) => State | Promise<State>.
{ pending: boolean; data: FormData | null; method: string; action: string | ((formData: FormData) => void) | null }data es el FormData siendo enviado (o null cuando no está pending)method refleja el método HTTP del formularioFormDataRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥