Formularios asíncronos en React 19 con use, useActionState y useOptimistic
Cómo use(), useActionState, useFormStatus y useOptimistic reemplazan la carga manual y las banderas de error en React 19, junto con los problemas que cada hook oculta silenciosamente.
Antes de React 19, casi todas las operaciones de fetch o formularios requerían la misma estructura básica: useState para almacenar los datos, useEffect para cargarlos, y variables indicadoras de estado como carga, errores y resultados. React 19 introduce cuatro APIs que integran estas funcionalidades directamente en el framework: use(), useActionState, useFormStatus y useOptimistic. A continuación se explica qué hace cada una, cómo funcionan en conjunto y las limitaciones que suelen pasarse por alto. Para obtener una visión general más amplia de la actualización, consulte React 19 upgrade triage.
Leer promesas durante el renderizado con use()
use() no es un gancho en el sentido tradicional. A diferencia de los ganchos, puede ser llamado dentro de condiciones, bucles o después de un retorno anticipado. Lee el valor de una Promise o de un Contexto durante la renderización y colabora con Suspense para mostrar un estado pendiente.
import { use, Suspense } from "react";
UserCard recibe una promise y la desempaca con use; mientras no esté resuelta, el límite más cercano de Suspense en Page muestra su contenido alternativo.
function UserCard({ userPromise }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}function Page({ userPromise }) {
return (
<Suspense fallback={<p>Loading user…</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
El componente padre crea la promise, el hijo la consume, y Suspense se encarga del estado de carga. Hay una restricción importante: la promise debe ser estable. Una promise creada durante la renderización de un componente cliente es nueva en cada renderización y provoca suspensiones repetidas, por lo que debe crearse en un componente servidor, en un cargador caché o en una API de datos del framework.
Manejo de envíos con useActionState
useActionState reemplaza los estados isSubmitting, error y result gestionados manualmente. Se pasa una acción y un estado inicial; se obtiene el estado actual, una acción envuelta para el formulario y una bandera que indica si hay pendientes tareas. La acción recibe el estado anterior y los datos de FormData, y lo que devuelva se convierte en el estado siguiente.
import { useActionState } from "react";
Aquí submitFeedback valida el mensaje, lo guarda y devuelve un marcador de error o de éxito, que el formulario muestra directamente.
async function submitFeedback(prevState, formData) {
const message = formData.get("message");
if (message.length < 5) {
return { error: "Message is too short." };
}
await saveFeedback(message);
return { success: true };
}function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {}); return (
<form action={formAction}>
<textarea name="message" required />
<button disabled={isPending}>
{isPending ? "Sending…" : "Send feedback"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p>Thanks for the feedback!</p>}
</form>
);
}
Dado que la acción se envía al atributo nativo action del formulario, esto permite la mejora progresiva. Tenga en cuenta que enviar los datos antes de la carga del contenido solo funciona cuando la acción es una función del servidor; una función puramente del cliente requiere que se cargue JavaScript. Además, formData.get devuelve null para un campo faltante, por lo que debe protegerse en el código real.
Leer el estado del formulario desde cualquier hijo con useFormStatus
Un botón de envío anidado profundamente en un formulario a menudo necesita saber si se está procesando el envío, y pasar la propiedad isPending por cada capa es tedioso. useFormStatus, importado de react-dom, lee el estado del <form> padre más cercano.
import { useFormStatus } from "react-dom";
SubmitButton no acepta propiedades; obtiene el estado pendiente del formulario que lo rodea, lo que lo hace ideal para componentes de botón compartidos.
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}function SettingsForm({ action }) {
return (
<form action={action}>
<input name="username" />
<SubmitButton />
</form>
);
}
El hook debe llamarse en un componente renderizado dentro de <form>. Llamarlo en el componente que renderiza el formulario en sí no devuelve ningún estado pendiente.
Mostrar los resultados esperados de inmediato con useOptimistic
useOptimistic muestra el resultado anticipado de una acción mientras se ejecuta la solicitud real.
import { useOptimistic, useRef } from "react";
En TodoList, el hook deriva optimisticTodos de la propiedad todos, añadiendo entradas marcadas como pending: true. La acción del formulario agrega el elemento optimista, reinicia el formulario mediante un ref y luego espera la acción real del servidor. Los elementos pendientes se muestran con media opacidad.
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const formRef = useRef(null); async function handleSubmit(formData) {
const title = formData.get("title");
addOptimisticTodo({ id: Date.now(), title });
formRef.current?.reset();
await addTodoAction(formData);
} return (
<div>
<form ref={formRef} action={handleSubmit}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
El modelo mental es preciso: el estado optimista solo existe mientras se ejecuta la acción. Cuando esta finaliza, React lo descarta y muestra lo que todos contiene actualmente. En caso de éxito, el componente padre debe proporcionar un todos actualizado (por ejemplo mediante revalidación); de lo contrario, el elemento desaparece. En caso de fallo, desaparece automáticamente, lo que constituye el mecanismo de reversión. Aún es necesario manejar los mensajes de error. El artículo sobre los modos de fallo de useOptimistic rollback aborda los casos límite.
Bonus: ref como prop normal
React 19 también permite que los componentes funcionales reciban ref como una prop ordinaria, por lo que forwardRef es innecesario en la mayoría de los casos. Esto elimina gran parte del encapsulamiento presente en las bibliotecas de componentes.
Puntos clave
use()desenvuelve las promesas y el contexto durante la renderización con Suspense, pero la promesa debe ser estable.useActionStateagrupa el estado, el resultado y una bandera de pendiente en un solo hook basado en formularios.useFormStatuslee el estado del formulario contenedor desde un elemento descendiente, eliminando la necesidad de pasar propiedades por niveles.useOptimisticmuestra los resultados esperados durante una acción y se basa en propiedades actualizadas para mantenerlos.- Antes de reconstruir una combinación de
useStateyuseEffect, verifica si alguno de estos casos se aplica.
Lecturas relacionadas
- Descontinuando el ref de último valor: Eventos de efecto con useEffectEvent — Aprenda cómo useEffectEvent en React 19.2 separa la lógica no reactiva de un efecto, elimina los trucos con el ref de último valor y determina cuándo este hook no es la herramienta adecuada.
- Triaje de la actualización a React 19: ¿Qué nuevas API sustituyen a las soluciones temporales que tiene? — Un análisis práctico de use, Server Actions, useOptimistic y el React Compiler, con las consideraciones importantes y un plan para adoptar primero qué cambios de React 19.