Escribí hace poco sobre revalidatePath vs revalidateTag en el cache de Next.js y me quedó una fricción que no resolví ahí: cuando la Server Action se ejecuta y el cache del server queda invalidado, la UI del cliente — si usás TanStack Query en paralelo — no se entera de nada hasta el próximo fetch. Hay un instante, corto pero real, donde el usuario ve el dato viejo mientras el server ya tiene el nuevo. Ese instante es el problema de hoy.
Mi tesis es simple: cuando necesitás que la UI reaccione en el momento — no en el próximo render, no después de que Next revalide su propio cache — invalidar manualmente con queryClient.setQueryData después de la Server Action gana contra confiar solo en revalidateTag. No porque revalidateTag esté mal. Porque resuelve un problema distinto: el cache del server, no el cache del cliente.
Lo digo así de directo porque es la parte que casi nadie deja explícita cuando combina las dos cosas: revalidateTag y setQueryData no compiten, viven en capas distintas, y confundirlas es lo que genera ese "a veces tarda en guardar" que nadie sabe explicar.
El problema concreto: dos caches que no se hablan
Cuando combinás Server Actions con TanStack Query en el cliente, tenés dos sistemas de cache corriendo en paralelo y sin acoplamiento automático:
- El cache de Next.js (
fetchcache, Data Cache, Router Cache) — lo manejarevalidatePathorevalidateTag. - El cache de TanStack Query en el cliente — vive en memoria del browser, con sus propias
queryKeyy su propiostaleTime.
Una Server Action puede invalidar perfectamente el primero y dejar el segundo intacto. El resultado: el server ya tiene el dato actualizado, pero el componente que lee con useQuery sigue mostrando lo que trajo el último fetch, hasta que algo dispare un refetch — un focus de ventana, un intervalo, una navegación. Ese "hasta que algo dispare" es el doble fetch disimulado: primero la Server Action, después — tarde — la query del cliente se pone al día.
Qué dice la documentación oficial (y qué no dice)
La guía de Optimistic Updates de TanStack Query plantea dos caminos para actualizar la UI antes de que el server confirme: usar el onMutate de useMutation para escribir directo en el cache con setQueryData, o usar variables de estado UI sin tocar el cache. La doc es clara en algo que muchos se saltan: si escribís en onMutate, tenés que guardar el snapshot anterior con getQueryData y devolverlo en el context para poder hacer rollback en onError. No es opcional, es parte del contrato del patrón.
Lo que la doc no dice — porque no es su tema — es cómo se combina esto con Server Actions de Next.js. TanStack Query asume que la mutación es una llamada a una API que vos controlás desde el cliente con mutationFn. Una Server Action no es eso: es una función que corre en el server y se invoca como si fuera local. El puente entre los dos mundos hay que armarlo a mano, y ahí está el punto ciego que este post trata de cerrar.
// hook de mutación que envuelve una Server Action
const queryClient = useQueryClient()
const { mutate } = useMutation({
mutationFn: actualizarPerfil, // Server Action
onMutate: async (nuevoPerfil) => {
await queryClient.cancelQueries({ queryKey: ['perfil'] })
const anterior = queryClient.getQueryData(['perfil'])
queryClient.setQueryData(['perfil'], nuevoPerfil) // update optimista
return { anterior }
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(['perfil'], context?.anterior) // rollback
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['perfil'] })
},
})El onSettled con invalidateQueries es la red de seguridad: pase lo que pase, se termina sincronizando con lo que devuelve el server. El setQueryData en onMutate es la parte que le da a la UI la sensación de instantaneidad.
Dónde se equivoca la gente
La receta común que veo repetida — en foros, en proyectos de ejemplo, en el propio scaffolding de Next.js con App Router — es confiar todo a revalidateTag dentro de la Server Action y asumir que "como el server ya revalidó, el cliente se va a enterar solo". Funciona si el componente que muestra el dato hace fetch directo dentro de un Server Component y el usuario navega o refresca. No funciona igual si ese mismo dato también vive en una query de TanStack en un Client Component, porque revalidateTag no tiene ninguna noción de que existe un queryClient corriendo en el browser.
El costo oculto es la sensación de lag: el usuario hace clic en "guardar", ve el spinner desaparecer, y el valor en pantalla tarda uno o dos segundos — o hasta el próximo focus de la ventana — en reflejar el cambio. No es un bug que rompa nada. Es una fricción de percepción, y esas son las que un usuario reporta como "a veces tarda en guardar" sin poder señalar por qué.
El contraejemplo que vale la pena tener en la cabeza: si el dato que estás actualizando no se lee después con useQuery en el cliente — por ejemplo, una Server Action que solo dispara un efecto y la página se re-renderiza server-side en la siguiente navegación — entonces setQueryData no aporta nada. Ahí revalidatePath o revalidateTag solos alcanzan, y meter TanStack Query es complejidad sin beneficio.
sequenceDiagram
participant U as Usuario
participant C as Cliente (TanStack Query)
participant S as Server Action
U->>C: Click guardar
C->>C: setQueryData (optimista)
C->>S: invoca Server Action
S->>S: revalidateTag / mutación en DB
S-->>C: respuesta (éxito o error)
alt error
C->>C: rollback con snapshot anterior
else éxito
C->>C: invalidateQueries (onSettled)
endChecklist de decisión
| Situación | Qué mirar primero | Decisión |
|---|---|---|
El dato se lee con useQuery en un Client Component y el usuario necesita feedback inmediato | ¿Hay riesgo real de que la mutación falle seguido? | setQueryData en onMutate + rollback en onError |
| El dato solo se muestra en Server Components y la página se re-renderiza en la próxima navegación | ¿Hay algún useQuery leyendo esa misma queryKey? | revalidateTag/revalidatePath solos, sin TanStack |
| La mutación toca datos que otros usuarios también ven (colaborativo) | ¿El optimismo puede mostrar un estado que nunca existió para el server? | Preferir invalidateQueries sin optimismo, aceptar el delay |
| El formulario es de alta frecuencia (autoguardado, likes, contadores) | ¿El costo de un rollback visible es aceptable? | setQueryData es casi obligatorio para que no se sienta trabado |
| Estás debuggeando por qué "a veces no se actualiza" | ¿Falta onSettled con invalidateQueries? | Agregarlo siempre, es la red de seguridad |
Esto no es una tabla de verdades universales, es un punto de partida para decidir con criterio según qué tan crítico es que el dato optimista coincida con la realidad.
Límites de esto
No tengo métricas propias de latencia percibida ni un experimento A/B que compare "con setQueryData" contra "sin él" en un caso productivo real — y no voy a inventarlas. Lo que puedo sostener es lo que la documentación oficial describe como contrato del patrón: snapshot, update, rollback condicional, invalidación final. Si necesitás cuantificar el impacto real en la experiencia de usuario, eso requiere logging de interacción o testing con usuarios reales, no algo que se resuelva leyendo un post.
Tampoco es un patrón gratis. Cada setQueryData optimista es una promesa que tu código le hace a la UI sobre cómo va a quedar el estado, y si esa promesa falla seguido — porque la Server Action rechaza la mutación por validación de negocio, no por error de red — el rollback se vuelve visible y molesto. En mutaciones con alta tasa de rechazo, el optimismo genera más ruido visual que el que ahorra. Ese es el trade-off que me parece honesto: optimismo a cambio de un rollback que a veces se ve feo.
Cuándo usarlo y cuándo no
Mi postura después de mirar la doc con este caso en la cabeza: usá setQueryData cuando el dato vive en el cliente vía useQuery y la latencia percibida importa más que la exactitud momentánea. No lo uses cuando el dato es compartido entre usuarios o cuando un rollback visible sería peor que un pequeño delay. Y siempre, sin excepción, cerrá el ciclo con invalidateQueries en onSettled — el optimismo sin red de seguridad es solo un bug esperando el momento equivocado para aparecer.
Si venís de leer sobre cómo Cline pone límites al modo autónomo vas a reconocer la misma lógica acá: automatizar sin control es una promesa que en algún momento se paga. El próximo paso lógico, si trabajás con este patrón, es instrumentar cuántas veces el rollback se dispara en desarrollo — no en producción todavía, solo para entender la tasa de fallo real de la mutación antes de decidir si el optimismo vale la pena en ese caso puntual.
Lo incómodo que dejo sobre la mesa: si tu mutación falla más de una vez cada diez intentos en desarrollo, el optimismo no es una mejora de UX, es una fuente nueva de bugs de percepción. Medí eso antes de copiar el patrón.
FAQ
¿setQueryData reemplaza a revalidateTag?
No. Resuelven cosas distintas: revalidateTag invalida el cache del server (Next.js), setQueryData actualiza el cache del cliente (TanStack Query). En un flujo con ambos sistemas, probablemente necesités los dos.
¿Qué pasa si no hago rollback en onError? El cache del cliente queda con un dato que nunca existió en el server. La próxima vez que algo dispare un refetch se corrige solo, pero mientras tanto el usuario ve información falsa.
¿Puedo usar esto con useOptimistic de React en vez de TanStack Query?
Sí, son herramientas distintas para necesidades parecidas. useOptimistic vive en el componente y no tiene noción de cache compartido entre queries; setQueryData sí, porque opera sobre el queryClient global.
¿Esto agrega latencia o la reduce? No cambia la latencia real de la mutación. Cambia la latencia percibida: la UI reacciona antes de que el server confirme.
¿Sirve para datos que vienen de un Server Component sin useQuery?
No. Si no hay una query de TanStack Query leyendo esa queryKey en el cliente, setQueryData no tiene nada para actualizar.
¿Hace falta invalidar igual si el optimismo ya mostró el dato correcto?
Sí. El invalidateQueries en onSettled no es redundante: es lo que garantiza que si el server devolvió algo distinto de lo que asumiste, el cliente se corrige.
Fuente original: https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates
Artículos Relacionados
fp-ts alternativas en TypeScript: cuándo vale la abstracción
Después de mostrar cómo un union type nativo reemplaza a Either y Option en la mayoría de los casos, toca la parte incómoda: decir dónde fp-ts sí gana. No es en todos lados, y esa es la postura.
08 sept 2026 · 7′ · Tutoriales · TypeScript · node.js
revalidatePath es fuerza bruta, revalidateTag es precisión
Confundí revalidatePath con revalidateTag en un proyecto chico y terminé invalidando páginas que no tenían nada que ver con el cambio. Acá está la diferencia real entre las dos granularidades de cache en Next.js 16, con checklist de decisión.
04 sept 2026 · 7′ · Tutoriales · Next.js · React
pnpm gana en monorepos, npm gana en fricción cero
La diferencia entre npm y pnpm no es velocidad de instalación. Es un modelo de almacenamiento distinto — content-addressable store vs árbol de carpetas — y ese modelo importa según el tamaño del proyecto, no según la moda.
29 ago 2026 · 8′ · Tutoriales · Next.js · pnpm
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.
Todavía nadie comentó. Sé el primero — tu opinión vale oro cuando somos pocos.