Cómo reducir requests duplicadas con TanStack Query
Estrategias de caché, deduplicación y placeholderData para bajar el tráfico a la API sin sacrificar UX.
Si tu app dispara 4 requests para mostrar el mismo dato, el problema rara vez es el backend.
En el producto educativo donde trabajo, varias vistas consultan las mismas listas: navegar entre pestañas o volver a un detalle repetía requests que ya se habían hecho segundos antes. Este es el enfoque que uso con TanStack Query para cortar esas duplicadas — sin tocar una línea del backend.
Lo que suele fallar del approach anterior
Con useEffect + fetch puro, cada componente que necesita una lista hace su
propio request, sin saber que otro componente ya la pidió hace 200 ms. Tres
patrones que aparecen una y otra vez:
- Refetch al cambiar de tab. El usuario navega entre “Inscritos / En proceso /
Archivados” y cada tab monta un
useEffectque repide la misma lista. - Sin caché entre rutas. Volver al detalle de un registro vuelve a pedir su data.
- Cero deduplicación. Dos componentes hermanos pidiendo lo mismo = dos requests.
La estrategia: convención de query keys por dominio
Lo primero no es código: es una convención. Cada feature exporta sus query keys desde un único archivo:
// features/students/api/keys.ts
export const studentsKeys = {
all: ["students"] as const,
lists: () => [...studentsKeys.all, "list"] as const,
list: (filters: StudentFilters) => [...studentsKeys.lists(), filters] as const,
details: () => [...studentsKeys.all, "detail"] as const,
detail: (id: string) => [...studentsKeys.details(), id] as const,
};
Esto resuelve dos cosas a la vez:
- Deduplicación: dos componentes con
useQuery({ queryKey: studentsKeys.list(filters) })comparten caché si losfiltersson iguales (TanStack Query hace deep equal). - Invalidación quirúrgica: tras crear un registro,
invalidateQueries({ queryKey: studentsKeys.lists() })invalida todas las listas sin tocar los detalles.
staleTime por naturaleza del dato
El default staleTime: 0 es agresivo: cualquier mount refetcha. Lo ajusto por
dominio según qué tan dinámico es el dato:
export function useStudents(filters: StudentFilters) {
return useQuery({
queryKey: studentsKeys.list(filters),
queryFn: () => fetchStudents(filters),
staleTime: 5 * 60 * 1000, // listas: 5 min de "fresh"
placeholderData: keepPreviousData, // UX fluido al paginar
});
}
export function useStudent(id: string) {
return useQuery({
queryKey: studentsKeys.detail(id),
queryFn: () => fetchStudent(id),
staleTime: 60 * 1000, // detalle: 1 min (puede editarse)
});
}
export function useCatalog() {
return useQuery({
queryKey: ["catalog"],
queryFn: fetchCatalog,
staleTime: Infinity, // catálogo: cambia un par de veces al año
});
}
La regla mental: ¿cuánto tiempo aceptaríamos ver un valor un poco viejo si eso evita una request? Para catálogos: horas. Para listas: minutos. Para detalles editables: segundos.
placeholderData: el truco más subestimado
Cuando el usuario pagina o cambia filtros, lo normal es ver un “loading…” y luego
los datos nuevos. Con keepPreviousData, ve los datos viejos hasta que lleguen
los nuevos. Cero flash, cero “vacío momentáneo”.
placeholderData: keepPreviousData,
Una línea. Pero psicológicamente cambia toda la sensación de rapidez de la app.
Lo que NO hago
- No meto
staleTime: Infinityen todo “para reducir requests”. Eso muestra data vieja y se vuelve un bug peor que el original. - No uso
enabled: falsepara resolver waterfalls. Esos se resuelven con prefetch o con composición de queries, no escondiendo la dependencia. - No invalido
studentsKeys.alldespués de cada mutación. Demasiado amplio — invalida hasta los detalles que no cambiaron.
El efecto
Con la convención de keys + staleTime por dominio + placeholderData, las
requests duplicadas al cambiar de tab desaparecen, la paginación se siente
instantánea y el backend recibe bastante menos tráfico — todo sin tocar su código.
Cuándo TanStack Query NO es la respuesta
Para estado local de UI (modales abiertos, tabs activos, drag state) sigo
usando useState o un store mínimo. TanStack Query es para estado del servidor,
no para estado del cliente. Confundir los dos lleva a código incomprensible.
Si te interesa cómo organizo las features para que esta convención escale, mira arquitectura modular por features.