React 19 de Cero a Experto / Datos
useMutation: escribir datos y sincronizar
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:
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:
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.