Server state — data that lives on a backend and changes without you — is fundamentally different from UI state. TanStack Query manages it: caching, deduping, background refresh, and stale-while-revalidate, all out of the box.
Querying Data
import { useQuery } from "@tanstack/react-query";
function Users() {
const { data, isPending, error } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then((r) => r.json()),
});
if (isPending) return <Spinner />;
if (error) return <Error />;
return data.map((u) => <li key={u.id}>{u.name}</li>);
}Mutations With Cache Invalidation
const qc = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }),
});Why Not Just useEffect + fetch?
- Automatic caching and request deduplication across components.
- Background refetch on window focus and reconnect keeps data fresh.
- Built-in retry, pagination, infinite scroll, and optimistic updates.
Pair It With Server Components
In Next.js, prefetch on the server and hydrate the query cache on the client — instant first paint plus all the client-side caching benefits.
