Inicio / Artículos

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.

Editor: REACTAPP.TOP4 min de lectura978 palabras

Fuentes: React 19’s New Hooks: A Practical Guide

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.
  • useActionState agrupa el estado, el resultado y una bandera de pendiente en un solo hook basado en formularios.
  • useFormStatus lee el estado del formulario contenedor desde un elemento descendiente, eliminando la necesidad de pasar propiedades por niveles.
  • useOptimistic muestra los resultados esperados durante una acción y se basa en propiedades actualizadas para mantenerlos.
  • Antes de reconstruir una combinación de useState y useEffect, verifica si alguno de estos casos se aplica.

Lecturas relacionadas