CodeForge

React 19 de Cero a Experto / Datos

useMutation: escribir datos y sincronizar

Teoría22 min20 XP

useQuery lee datos; useMutation los ESCRIBE —crear, editar, borrar—. Y no solo dispara el POST: se integra con la caché para que, tras escribir, la UI se sincronice sola (con invalidateQueries) y para que puedas mostrar el cambio AL INSTANTE con actualizaciones optimistas —el useOptimistic del módulo 4, pero a nivel de toda la caché—. Hoy cierras el ciclo completo: leer y escribir datos del servidor sin bugs de sincronización.

El mesero que lleva tu pedido a la cocina

Lo básico: mutar e invalidar

useMutation recibe una mutationFn (la que escribe) y devuelve un mutate para dispararla, más estados como isPending. El patrón más común: al tener éxito, invalidas la query afectada:

usemutation.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";

function FormularioGasto() {
const queryClient = useQueryClient();

const mutacion = useMutation({
  mutationFn: (nuevo: NuevoGasto) =>
    fetch("/api/gastos", { method: "POST", body: JSON.stringify(nuevo) }).then((r) => r.json()),
  onSuccess: () => {
    // al confirmar, marca la lista como vieja → se recarga sola en toda la app
    queryClient.invalidateQueries({ queryKey: ["gastos"] });
  },
});

return (
  <button
    onClick={() => mutacion.mutate({ nombre: "Café", valor: 4000 })}
    disabled={mutacion.isPending}
  >
    {mutacion.isPending ? "Guardando…" : "Agregar"}
  </button>
);
}

mutacion.mutate(datos) dispara la escritura; isPending te da el estado de "guardando" sin useState manual. Y el onSuccess con invalidateQueries es el patrón estrella: tras escribir, invalidas la lista y TanStack la recarga —toda la UI que muestre esos gastos se sincroniza sola—. Es el ciclo leer-escribir-sincronizar resuelto sin coordinar estados a mano.

Actualizaciones optimistas

Para que la UI se sienta instantánea, actualizas la caché ANTES de que el servidor confirme, con onMutate, y reviertes en onError. Es el useOptimistic del módulo 4, pero sobre la caché compartida:

optimista.tsx
const mutacion = useMutation({
mutationFn: borrarGasto,

// 1. ANTES de que el servidor responda: actualiza la caché ya
onMutate: async (id: number) => {
  await queryClient.cancelQueries({ queryKey: ["gastos"] });     // frena revalidaciones en curso
  const previo = queryClient.getQueryData<Gasto[]>(["gastos"]);  // guarda el estado actual
  queryClient.setQueryData<Gasto[]>(["gastos"], (viejo) =>
    (viejo ?? []).filter((g) => g.id !== id)                     // quita el gasto YA (optimista)
  );
  return { previo }; // contexto para poder revertir
},

// 2. si falla: revierte al estado guardado
onError: (_err, _id, contexto) => {
  if (contexto) queryClient.setQueryData(["gastos"], contexto.previo);
},

// 3. pase lo que pase: revalida para quedar en sync con el servidor
onSettled: () => queryClient.invalidateQueries({ queryKey: ["gastos"] }),
});

Los tres ganchos forman el patrón optimista completo: onMutate cambia la caché al instante y guarda el estado previo; onError lo restaura si el servidor rechaza; onSettled revalida al final para garantizar que la UI coincide con el servidor real. Es más ceremonia que el useOptimistic del módulo 4, pero opera sobre la caché GLOBAL —el cambio optimista se ve en TODOS los componentes que usan ["gastos"], no solo en un formulario—.

Tras un useMutation que crea un gasto con POST, ¿cómo logras que la lista en pantalla (y en toda la app) se actualice?

Mini-reto

Diseña (en papel o en tu proyecto) la mutación de "editar gasto": 1) la mutationFn que hace PUT /api/gastos/:id; 2) un onSuccess que invalide ["gastos"] y también ["gasto", id]; 3) bosqueja la versión optimista: en onMutate, actualiza la caché de ["gastos"] reemplazando el gasto editado, guarda el previo, y en onError restáuralo. Explica qué ve el usuario en cada paso si el servidor tarda 2 segundos y luego falla.

Qué sigue

Ya lees y escribes datos con TanStack. Pero hay una forma aún más elegante de manejar la carga y los errores: dejar que React los coordine a nivel de árbol con SUSPENSE y ERROR BOUNDARIES, en vez de comprobar isLoading/isError en cada componente. La próxima lección —con demo en vivo— muestra esa otra cara de la carga de datos.