Tenés un formulario que llama a una Server Action. Funciona. El servidor procesa, revalida el path con revalidatePath, y la UI se actualiza. Hasta ahí, todo bien. El problema aparece cuando ese mismo dato lo necesitás en tres componentes distintos que no comparten árbol de renderizado, y cada uno tiene que enterarse del cambio sin que refresques la página entera ni dupliques fetches.
Ahí es donde vi trabarse a más de un equipo: siguen empujando revalidatePath a lo bruto, invalidan más de lo que deberían, y terminan con un árbol de componentes que refetchea todo cada vez que cualquier cosa cambia. Server Actions no tiene un modelo de cache client-side. No tiene staleTime, no tiene invalidación granular por query key, no sabe qué componentes están "escuchando" ese dato. Y no tiene por qué tenerlo — no es su trabajo.
Mi tesis, la que sostengo después de armar esta arquitectura varias veces, es esta: Server Actions resuelve la mutación, pero no reemplaza un cache client-side inteligente. Lo incómodo de esto es que el marketing alrededor de Server Actions vende la idea de que "ya no necesitás React Query", y eso es cierto solo para el subconjunto de casos donde un componente es dueño exclusivo del dato. La pregunta real no es "Server Actions o TanStack Query", es dónde traza cada uno la línea, y qué pasa cuando las combinás bien.
Server Actions y TanStack Query en Next.js App Router: qué resuelve cada uno
Server Actions es un mecanismo de RPC del servidor hacia el cliente: ejecutás código en el servidor desde un formulario o un handler, sin escribir una ruta de API explícita. Es excelente para mutaciones simples — crear, actualizar, borrar — donde el flujo es "el usuario hace algo, el servidor lo procesa, la UI refleja el resultado".
Lo que Server Actions no trae de fábrica:
- Cache client-side con TTL configurable
- Revalidación optimista (mostrar el resultado esperado antes de que el servidor confirme)
- Deduplicación de requests entre componentes que piden lo mismo
- Refetch automático en focus de ventana o reconexión de red
- Estados de loading/error granulares por query, reutilizables en cualquier componente
TanStack Query fue construido específicamente para esos cinco puntos. La documentación oficial lo describe como una librería de "manejo de estado async del servidor" — no gestiona estado de UI, gestiona el ciclo de vida de datos que viven en otro lado y necesitás sincronizar.
La combinación natural en Next.js 16 App Router es: Server Components para el fetch inicial (SSR, sin JS en el cliente), Server Actions para las mutaciones, y TanStack Query en los componentes cliente que necesitan reactividad — refetch, cache compartido, invalidación cruzada.
// hook que envuelve la Server Action con TanStack Query
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { actualizarTarea } from '@/actions/tareas'
export function useActualizarTarea() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: actualizarTarea, // la Server Action tal cual
onMutate: async (nuevaTarea) => {
await queryClient.cancelQueries({ queryKey: ['tareas'] })
const anterior = queryClient.getQueryData(['tareas'])
queryClient.setQueryData(['tareas'], (old: any) =>
old.map((t: any) => t.id === nuevaTarea.id ? nuevaTarea : t)
)
return { anterior }
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(['tareas'], context?.anterior)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['tareas'] })
},
})
}Lo que hace este hook: llama a la Server Action como mutationFn (a TanStack Query no le importa si es un fetch a una API REST o una Server Action — para él es una promesa), actualiza el cache local de forma optimista antes de que el servidor responda, y si falla, revierte. Eso es lo que Server Actions sola no te da: la Server Action confirma o falla, pero no maneja qué mostraba la UI mientras esperaba.
Dónde se equivoca la gente con este patrón
La receta común que veo repetida: alguien lee que Server Actions "reemplaza React Query" porque simplifica las mutaciones, y saca la librería del proyecto entero. Después de un tiempo, aparecen dos síntomas.
El primero es el refetch a lo bruto. Sin cache client-side, cada componente que necesita el mismo dato dispara su propio fetch — no hay deduplicación, no hay estado compartido. Si tenés un dashboard con cuatro widgets que leen la misma tabla, son cuatro requests idénticos en el mismo render.
El segundo es la falta de estado optimista real. useOptimistic de React 19 te da algo parecido, pero es local al componente que lo declara — no sincroniza con otros componentes que muestran el mismo dato en otra parte del árbol. Si necesitás que un cambio en un modal se refleje instantáneamente en una lista que vive en otro layout, useOptimistic sin cache compartido no te alcanza.
El contraejemplo más claro es un formulario de edición simple, un solo componente, una sola lectura después de guardar. Ahí meter TanStack Query es peso muerto: agregás una dependencia, un QueryClientProvider, y una capa de indirección para un caso donde revalidatePath + useOptimistic ya resuelve todo. El costo oculto de sumar una librería no es solo el bundle — es la superficie mental que cualquiera que toque ese código tiene que entender. Y esa superficie la pagás vos en el próximo code review, no el que decidió meterla.
Matriz de decisión: cuándo sumar TanStack Query sobre Server Actions
| Escenario | Server Actions solo | + TanStack Query |
|---|---|---|
| Mutación en un formulario, un solo consumidor del dato | Alcanza | Innecesario |
| Mismo dato leído por 3+ componentes client-side desincronizados | Refetch duplicado, sin cache compartido | Resuelve con queryKey compartida |
| Necesitás refetch en focus/reconexión de red | No lo tiene nativo | refetchOnWindowFocus de fábrica |
| Revalidación optimista cross-componente | useOptimistic es local al componente | onMutate + cache global |
| Fetch inicial de página, sin interacción posterior | Server Component puro alcanza | No aporta nada |
| Polling o datos que cambian fuera de la acción del usuario | Necesitás lógica propia de intervalos | refetchInterval nativo |
| Paginación o scroll infinito con cache por página | Requiere estado manual | useInfiniteQuery resuelve el patrón completo |
La pregunta que me hago primero, antes de decidir, es: ¿este dato lo necesita más de un componente client-side que no comparte estado por props? Si la respuesta es no, no sumo la librería. Si es sí, la Server Action queda como mutationFn y TanStack Query maneja el resto.
flowchart LR
A[Necesito mutar datos] --> B{¿Un solo consumidor client-side?}
B -->|sí| C[Server Action + useOptimistic]
B -->|no, varios componentes leen el mismo dato| D{¿Necesito refetch automático o cache compartido?}
D -->|no| C
D -->|sí| E[Server Action como mutationFn + TanStack Query]Los límites de esta guía
Esta matriz es criterio, no medición. No tengo benchmarks de bundle size ni de tiempo de renderizado comparando ambos enfoques en un proyecto real — eso requeriría un experimento reproducible con Lighthouse o next build --profile sobre un caso concreto, y no lo tengo para mostrar acá. Si te importa el peso exacto que agrega TanStack Query al bundle del cliente, corré next build con y sin la librería y compará el output de .next/analyze — ese es el experimento, no un número que yo te tire sin fuente.
Tampoco puedo afirmar que este patrón sea "la forma correcta" en todo proyecto. Depende del tamaño del equipo, de cuántos componentes cliente coexisten leyendo el mismo estado, y de si el proyecto ya tiene otra solución de estado global (Zustand, Jotai, context custom) que resuelve parte de lo mismo. La documentación oficial de TanStack Query no dice "usá esto siempre sobre Server Actions" — dice que resuelve estado async del servidor, y ahí termina la recomendación oficial.
Mi postura
No elijo uno u otro como bandera. Uso Server Actions para toda mutación donde un componente es dueño exclusivo del dato — ahí useOptimistic y revalidatePath alcanzan y no meto una dependencia más. Sumo TanStack Query en el momento exacto donde dos o más componentes client-side necesitan el mismo dato sincronizado sin pasarlo por props ni duplicar el fetch. Ese es el límite que trazo, y lo trazo antes de escribir el primer hook, no después de descubrir que el dashboard hace cuatro requests iguales.
Si te venden "sacá React Query, Server Actions lo resuelve todo" como regla general, esa frase esconde el caso de uso del que la dice, no el tuyo. Preguntá cuántos componentes cliente leen ese dato antes de sacar nada.
Si estás decidiendo la arquitectura de datos de un proyecto Next.js 16 nuevo, el mismo criterio de "no sumar una capa sin necesidad concreta" aplica en otros lugares del stack — lo escribí en detalle pensando en cuándo los path aliases de tsconfig ayudan y cuándo rompen el build sin aviso. Y si el problema que tenés no es de cache sino de tipos que representan estados alternativos (éxito/error, presente/ausente), esa discusión es otra — la charlé en fp-ts Either y Option como alternativa en TypeScript y en functional programming con TypeScript y lo que fp-ts enseña.
FAQ
¿TanStack Query reemplaza a Server Actions en Next.js 16?
No. Son capas distintas. Server Actions ejecuta la mutación en el servidor; TanStack Query gestiona el cache y la sincronización de ese dato en el cliente. Podés usar la Server Action como mutationFn dentro de useMutation.
¿Puedo usar TanStack Query solo para el fetch y Server Actions solo para mutar?
Sí, y es un patrón común: useQuery con una función que llama a un Server Component exportado como acción de lectura, o directamente a un endpoint, y useMutation envolviendo la Server Action de escritura.
¿Necesito TanStack Query si mi app es chica?
Si un solo componente es dueño del dato y no hay refetch cruzado, useOptimistic más revalidatePath alcanza sin sumar dependencias. La matriz de esta guía te ayuda a decidir según cuántos consumidores tiene el dato.
¿Qué diferencia hay entre revalidatePath y invalidateQueries?
revalidatePath invalida el cache del Server Component en el servidor y fuerza un nuevo render en el próximo request. invalidateQueries marca como stale una query específica en el cache client-side de TanStack Query y dispara un refetch si hay observadores activos. Operan en capas distintas del stack.
¿TanStack Query funciona con streaming de Server Components en Next.js 16?
Sí, siempre que el componente que usa useQuery sea client-side ('use client'). El streaming del servidor no interfiere con el cache del cliente porque son mecanismos independientes.
¿Hay overhead de bundle real al agregar TanStack Query?
Existe, como con cualquier librería. No tengo una cifra exacta para citar sin fuente — el experimento reproducible es correr next build con y sin la dependencia y comparar el reporte de bundle analyzer en ese proyecto puntual.
Fuente original: TanStack Query Documentation
Artículos Relacionados
Cline en producción: el agente de código autónomo para VS Code que uso con restricciones deliberadas
Cline puede crear archivos, ejecutar comandos y abrir el browser de forma autónoma desde VS Code. Eso suena a productividad. También huele a riesgo si no sabés qué permisos le das antes de empezar. Mi tesis: el modelo mental importa más que la herramienta.
17 ago 2026 · 9′ · Tutoriales · TypeScript · LLM
tsconfig paths en Next.js 16 App Router: cuándo ayudan y cuándo rompen el build sin aviso
Los path aliases parecen inocentes hasta que el build de producción falla sin mensaje claro. Documenté los 3 casos de rotura más comunes en un monorepo con Next.js 16 App Router y TypeScript estricto, y el patrón de configuración que sobrevivió.
14 ago 2026 · 9′ · Tutoriales · TypeScript · pnpm
¿De verdad necesitás fp-ts, o te alcanza un union nativo?
Escribí sobre fp-ts esta semana y me quedó la duda incómoda: ¿hacía falta toda esa maquinaria? Análisis de cuándo un discriminated union nativo resuelve lo mismo que Either/Option sin la curva.
14 ago 2026 · 7′ · Tutoriales · TypeScript · arquitectura de software
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.