Blog
4 min de lectura

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.

TanStack Query React Performance Arquitectura

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:

  1. Refetch al cambiar de tab. El usuario navega entre “Inscritos / En proceso / Archivados” y cada tab monta un useEffect que repide la misma lista.
  2. Sin caché entre rutas. Volver al detalle de un registro vuelve a pedir su data.
  3. 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 los filters son 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: Infinity en todo “para reducir requests”. Eso muestra data vieja y se vuelve un bug peor que el original.
  • No uso enabled: false para resolver waterfalls. Esos se resuelven con prefetch o con composición de queries, no escondiendo la dependencia.
  • No invalido studentsKeys.all despué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.