What is TanStack Query?
TanStack Query, formerly React Query, is a server-state management library. It recognises that data fetched from an API is fundamentally different from the state you keep in useState. Server data is shared, cached, can go stale, and needs to be refetched. Managing that with effects and flags is tedious and bug-prone.
TanStack Query gives server data a proper cache. You describe how to fetch a piece of data and give it a key, and the library handles caching, deduplication, loading and error state, background refetching and invalidation. It is the missing data layer for React, and it pairs with any client-state library.
Queries
A query is defined by a key and a query function. The key identifies the cache entry; the function fetches the data.
// 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>;
}
Every component that uses the same key shares one cache entry, so mounting the component twice fetches once. The status flags are provided, so you never write them yourself.
Query keys and the cache
The key is the identity of the cache entry, and it should include every variable the fetch depends on.
// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
queryKey: ["posts", { page, status }],
queryFn: () => fetchPosts({ page, status }),
});
Because keys are arrays, you can invalidate a whole family with a prefix. Invalidating ["posts"] also invalidates ["posts", { page: 1 }], which makes cache updates predictable.
Freshness and refetching
Two options control when data is refetched.
// 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 is how long data is considered fresh; while fresh, no background refetch happens. gcTime is how long an unused entry stays in memory before being garbage collected. By default, queries refetch on mount, on window focus and on reconnect, which keeps the UI current without any manual work.
Mutations
useMutation handles writes: create, update and delete.
// 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>
);
}
After a successful mutation, invalidating the affected queries tells every consumer to refetch. This keeps the server as the source of truth and avoids manual cache surgery.
Optimistic updates
For actions that should feel instant, update the cache before the server responds and roll back on failure.
// 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 applies the optimistic change and snapshots the previous value, onError rolls back, and onSettled refetches to reconcile with the server.
Pagination and infinite queries
TanStack Query has first-class support for paginated and infinite data. useQuery with a keepPreviousData option makes page transitions smooth, and useInfiniteQuery handles cursor-based lists with a fetchNextPage function. Because each page is part of the cache, back and forward navigation is instant.
Server state versus client state
This is the distinction that makes TanStack Query click:
- Server state is owned by the server, shared, asynchronous and can go stale. Use TanStack Query.
- Client state is owned by the browser, synchronous and local. Use
useState, Context, Zustand or Redux.
Mixing the two in one store is the source of a lot of complexity. Splitting them — TanStack Query for data, a small store for UI state — keeps each simple.
Best practices
- Include every variable in the query key.
- Set
staleTimedeliberately; the default of zero refetches aggressively. - Use mutations plus invalidation rather than hand-updating the cache everywhere.
- Keep query functions small and colocated with the query.
- Add optimistic updates only where instant feedback matters.
- Use the devtools to inspect cache state while developing.
- Keep server state in TanStack Query and client state elsewhere.
Common mistakes
- Using the same key for different inputs and serving stale or wrong data.
- Leaving
staleTimeat zero and triggering constant refetches. - Duplicating server data into a global store.
- Forgetting to invalidate after a mutation and showing outdated data.
- Writing optimistic updates without a rollback path.
- Treating query functions as components and calling hooks inside them.
Where to go next
TanStack Query is the standard way to handle server data in modern apps. Pair it with the Fetch API for the request layer, a client store like Zustand for UI state, and Redux Toolkit if you need strict global state. For Vue, Pinia handles client state while TanStack Query covers the server side.