CodeForge

React 19 de Cero a Experto / React 19 moderno

Actions y useActionState: formularios que se manejan solos

Teoría24 min20 XP

Enviar un formulario a un servidor siempre llevó el mismo ritual manual: un estado enviando, un estado error, un try/catch, poner y quitar el disabled… en cada formulario, una y otra vez. React 19 lo automatiza con las ACTIONS y el hook useActionState: le das la función que hace el trabajo y React te maneja el estado pendiente y el resultado. Hoy ves el "antes" doloroso —que corre en vivo— y el "después" de React 19. Es uno de los cambios más prácticos del framework.

El mesero que anota y te avisa

El "antes": todo a mano (corre en vivo)

Así se hacía —y se sigue haciendo en React 18—: cada pedazo de estado, manual. Este playground funciona; fíjate en cuánta contabilidad hay para algo tan común:

Cuenta los estados: enviando, error, ok, más el try/catch/finally y el no-olvidar-apagar el disabled. Es correcto, pero es el MISMO boilerplate en cada formulario de la app. Aquí es donde React 19 dice "yo me encargo".

El "después": useActionState

Una Action es simplemente una función asíncrona que recibe los datos del formulario y hace el trabajo. useActionState la envuelve y te devuelve el estado del resultado, la versión "envolvida" de la acción para el <form>, y —gratis— un booleano pendiente:

con-useactionstate.tsx
import { useActionState } from "react";

type Estado = { error?: string; ok?: boolean };

// Una ACTION: recibe el estado previo y el FormData del formulario.
async function guardarGasto(_previo: Estado, formData: FormData): Promise<Estado> {
const nombre = String(formData.get("nombre") ?? "");
if (!nombre.trim()) return { error: "El nombre es obligatorio" };
await guardarEnServidor(nombre);
return { ok: true };
}

export default function App() {
// [estado del resultado, la acción para el form, ¿está pendiente?]
const [estado, accionForm, pendiente] = useActionState(guardarGasto, {});

return (
  <form action={accionForm} style={{ padding: 24 }}>
    <input name="nombre" placeholder="Nombre del gasto" />
    {/* 'pendiente' lo maneja React: true mientras la action corre */}
    <button disabled={pendiente}>{pendiente ? "Guardando…" : "Guardar"}</button>
    {estado.error && <p style={{ color: "#ef4444" }}>⚠ {estado.error}</p>}
    {estado.ok && <p style={{ color: "#22c55e" }}>✓ Guardado</p>}
  </form>
);
}

Compara con el "antes": desaparecen los useState de enviando, error y ok, el preventDefault, el try/finally para apagar el disabled. useActionState(fn, estadoInicial) te da tres cosas: el estado que devuelve tu acción, una accionForm que pasas a <form action={...}>, y pendiente que React pone en true mientras la acción corre. Tú solo escribes la lógica del envío y devuelves el nuevo estado; el seguimiento lo lleva React.

Cómo encajan las piezas

Un apunte de honestidad

¿Qué te devuelve useActionState(accion, estadoInicial) y qué maneja React por ti?

Mini-reto

En tu proyecto React 19 (o en papel, razonando el flujo): 1) escribe una acción suscribir(previo, formData) que lea un email del FormData, valide que contenga "@" y devuelva { error } o { ok: true }; 2) conéctala con useActionState y un <form action={...}> con un input name="email"; 3) usa el tercer valor pendiente para deshabilitar el botón y mostrar "Enviando…". Compara cuántas líneas te ahorras frente al "antes" manual del playground.

Qué sigue

Ya delegas el estado pendiente. Pero hay una experiencia aún más pulida: que la UI responda AL INSTANTE, antes de que el servidor confirme —y se corrija sola si algo falla—. Eso es la UI optimista, y React 19 trae un hook dedicado: useOptimistic. La próxima lección lo desarma.