¿Qué es TanStack Query?
TanStack Query, anteriormente conocido como React Query, es una librería de gestión de estado del servidor. Parte de la premisa de que los datos obtenidos de una API son fundamentalmente diferentes del estado que mantienes en useState. Los datos del servidor son compartidos, se almacenan en caché, pueden quedar obsoletos y necesitan ser vueltos a solicitar. Gestionar esto mediante efectos y banderas es tedioso y propenso a errores.
TanStack Query proporciona una caché adecuada para los datos del servidor. Tú describes cómo obtener una pieza de datos y le asignas una clave, y la librería se encarga de la caché, la deduplicación, los estados de carga y error, la actualización en segundo plano y la invalidación. Es la capa de datos que le faltaba a React y se complementa con cualquier librería de estado del cliente.
Queries
Una query se define mediante una key y una función de query. La key identifica la entrada en la caché; la función se encarga de obtener los datos.
// api.ts
async function fetchPost(id: string) {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Post.tsx
import { useQuery } from "@tanstack/react-query";
export function Post({ id }: { id: string }) {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
});
if (isLoading) return <p>Loading…</p>;
if (isError) return <p>Error: {error.message}</p>;
return <article>{data.title}</article>;
}
Cada componente que utilice la misma key comparte una única entrada de caché, por lo que montar el componente dos veces solo dispara una petición. Se proporcionan flags de estado, así que nunca tienes que escribirlos tú mismo.
Claves de consulta y la caché
La clave es la identidad de la entrada de la caché y debe incluir cada variable de la que dependa la petición.
// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
queryKey: ["posts", { page, status }],
queryFn: () => fetchPosts({ page, status }),
});
Dado que las claves son arrays, puedes invalidar una familia completa mediante un prefijo. Invalidar ["posts"] también invalida ["posts", { page: 1 }], lo que hace que las actualizaciones de la caché sean predecibles.
Frescura y refetching
Hay dos opciones que controlan cuándo se vuelven a solicitar los datos.
// freshness.ts
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 60_000, // fresh for one minute
gcTime: 5 * 60_000, // keep unused data for five minutes
});
staleTime define cuánto tiempo se consideran “frescos” los datos; mientras estén frescos, no se realizará ningún refetch en segundo plano. gcTime define cuánto tiempo permanece una entrada sin usar en la memoria antes de ser eliminada por el recolector de basura. Por defecto, las queries realizan un refetch al montarse, al recuperar el foco de la ventana y al reconectarse, lo que mantiene la UI actualizada sin necesidad de trabajo manual.
Mutations
useMutation se encarga de las escrituras: crear, actualizar y eliminar.
// AddPost.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
}).then((r) => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return (
<button
onClick={() => mutation.mutate("New post")}
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving…" : "Add post"}
</button>
);
}
Después de una mutación exitosa, invalidar las queries afectadas indica a cada consumidor que debe volver a realizar la petición (refetch). Esto mantiene al servidor como la fuente de verdad y evita tener que manipular la caché manualmente.
Actualizaciones optimistas
Para aquellas acciones que deben sentirse instantáneas, actualiza la caché antes de que el servidor responda y revierte los cambios en caso de error.
// optimistic.ts
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ["post", postId] });
const previous = queryClient.getQueryData(["post", postId]);
queryClient.setQueryData(["post", postId], (old) => ({
...old,
likes: old.likes + 1,
}));
return { previous };
},
onError: (_err, postId, context) => {
queryClient.setQueryData(["post", postId], context.previous);
},
onSettled: (_data, _err, postId) => {
queryClient.invalidateQueries({ queryKey: ["post", postId] });
},
});
onMutate aplica el cambio optimista y guarda una instantánea del valor anterior, onError revierte los cambios y onSettled vuelve a solicitar los datos para sincronizarlos con el servidor.
Paginación y consultas infinitas
TanStack Query ofrece soporte nativo para datos paginados e infinitos. useQuery con la opción keepPreviousData hace que las transiciones entre páginas sean fluidas, y useInfiniteQuery gestiona listas basadas en cursores mediante la función fetchNextPage. Debido a que cada página forma parte de la caché, la navegación hacia adelante y hacia atrás es instantánea.
Estado del servidor frente a estado del cliente
Esta es la distinción que hace que TanStack Query tenga sentido:
- Estado del servidor: pertenece al servidor, es compartido, asíncrono y puede quedar desactualizado (stale). Usa TanStack Query.
- Estado del cliente: pertenece al navegador, es síncrono y local. Usa
useState, Context, Zustand o Redux.
Mezclar ambos en un mismo store es el origen de mucha complejidad. Separarlos —TanStack Query para los datos y un store pequeño para el estado de la UI— mantiene cada parte simple.
Mejores prácticas
- Incluye cada variable en la query key.
- Define
staleTimedeliberadamente; el valor predeterminado de cero provoca refetches agresivos. - Utiliza mutations junto con invalidación en lugar de actualizar manualmente la caché en todas partes.
- Mantén las funciones de query pequeñas y ubicadas junto a la query.
- Añade optimistic updates solo donde la respuesta instantánea sea fundamental.
- Utiliza las devtools para inspeccionar el estado de la caché durante el desarrollo.
- Mantén el estado del servidor en TanStack Query y el estado del cliente en otro lugar.
Errores comunes
- Usar la misma clave para diferentes inputs, lo que provoca la entrega de datos obsoletos o incorrectos.
- Dejar
staleTimeen cero, provocando refetches constantes. - Duplicar datos del servidor en un store global.
- Olvidar invalidar después de una mutación, mostrando datos desactualizados.
- Implementar optimistic updates sin un camino de rollback.
- Tratar las funciones de query como componentes y llamar a hooks dentro de ellas.
Próximos pasos
TanStack Query es la forma estándar de manejar datos del servidor en aplicaciones modernas. Combínalo con la Fetch API para la capa de peticiones, un store de cliente como Zustand para el estado de la UI, y Redux Toolkit si necesitas un estado global estricto. Para Vue, Pinia se encarga del estado del cliente mientras que TanStack Query cubre la parte del servidor.