TanStack Query v5: The New API and Why stale-while-revalidate Changes Everything
TanStack Query v5 unifies the hook API, adds queryOptions() for type-safe reuse, and makes stale-while-revalidate the default mental model for server state in React.
Mahmudul Haque Qudrati
CEO & ML Engineer
One AI engineering post, weekly
LLM benchmarks, prompt techniques, and token-cost breakdowns — not another AI news roundup.
TanStack Query (formerly React Query) is built on a simple insight: server data in a UI is always potentially stale. Instead of blocking the user while you re-fetch, show the cached data immediately and update it in the background. This is stale-while-revalidate, and it makes your app feel instant.
v5 Unified API
v5 flattened the options API. Most commonly-used options are now top-level:
// v4
const { data } = useQuery(["users", id], () => fetchUser(id), {
staleTime: 1000 * 60,
keepPreviousData: true,
});
// v5 - unified object, new option names
const { data } = useQuery({
queryKey: ["users", id],
queryFn: () => fetchUser(id),
staleTime: 1000 * 60,
placeholderData: "keepPrevious", // replaces keepPreviousData
});
Team workspace
Ship faster with chat, meetings, and projects in one place — Zlyqor.
queryOptions() for Type-Safe Reuse
Define queries once and reuse them in hooks and prefetch calls with full type inference:
// lib/queries/tasks.ts
import { queryOptions } from "@tanstack/react-query";
export const taskQueries = {
list: (projectId: string) =>
queryOptions({
queryKey: ["tasks", projectId],
queryFn: () => api.tasks.list(projectId),
staleTime: 30_000,
}),
detail: (taskId: string) =>
queryOptions({
queryKey: ["tasks", "detail", taskId],
queryFn: () => api.tasks.get(taskId),
}),
};
// In a component
const { data: tasks } = useQuery(taskQueries.list(projectId));
// In a server component for prefetch
await queryClient.prefetchQuery(taskQueries.list(projectId));
// In a mutation's onSuccess
queryClient.invalidateQueries(taskQueries.list(projectId));
Optimistic Updates
const createTask = useMutation({
mutationFn: (data: NewTask) => api.tasks.create(data),
onMutate: async (newTask) => {
// Cancel in-flight refetches
await queryClient.cancelQueries(taskQueries.list(projectId));
// Snapshot current data
const previous = queryClient.getQueryData(taskQueries.list(projectId).queryKey);
// Optimistically update
queryClient.setQueryData(
taskQueries.list(projectId).queryKey,
(old: Task[]) => [...old, { ...newTask, id: "temp", status: "pending" }]
);
return { previous };
},
onError: (err, vars, context) => {
// Roll back on error
queryClient.setQueryData(
taskQueries.list(projectId).queryKey,
context?.previous
);
},
onSettled: () => {
queryClient.invalidateQueries(taskQueries.list(projectId));
},
});
Suspense Mode
v5 stabilizes Suspense integration - no more experimental flags:
import { useSuspenseQuery } from "@tanstack/react-query";
function TaskList({ projectId }: { projectId: string }) {
// Throws a promise while loading - caught by parent Suspense
const { data: tasks } = useSuspenseQuery(taskQueries.list(projectId));
return <ul>{tasks.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
// Parent
<Suspense fallback={<Skeleton />}>
<ErrorBoundary fallback={<Error />}>
<TaskList projectId={id} />
</ErrorBoundary>
</Suspense>
SSR With Next.js App Router
// app/projects/[id]/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { taskQueries } from "@/lib/queries/tasks";
export default async function ProjectPage({ params }: { params: { id: string } }) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(taskQueries.list(params.id));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TaskList projectId={params.id} />
</HydrationBoundary>
);
}
TanStack Query vs SWR
| Feature | TanStack Query | SWR |
|---|---|---|
| Bundle size | ~13KB | ~4KB |
| Mutations | Full API | Manual |
| Optimistic updates | Built-in | Manual |
| Devtools | First-class | Third-party |
| Infinite queries | Built-in | Built-in |
| Suspense | Stable | Experimental |
| Learning curve | Medium | Low |
SWR is great for simple fetching. TanStack Query wins when you need mutations, optimistic updates, or complex cache management.
References: TanStack Query · GitHub · v5 migration
Mahmudul Haque Qudrati
CEO & ML Engineer
Visionary leader with extensive experience in machine learning and software development. Drives strategic innovation and business growth.
More from Mahmudul
Related Articles
How to Build an MCP Server in TypeScript (Step-by-Step)
Build your first MCP server in TypeScript with the official SDK — tools, stdio transport, and a Claude Code test loop in under an hour.
Redis Guide for Developers: Not Just a Cache
Redis is an in-memory data structure store used for caching, sessions, rate limiting, queues, pub/sub, and distributed locks. Here is how to use it well.
React Server Components: What They Are and When to Use Them
React Server Components run only on the server and never ship to the browser. Here is what that means in practice and when it actually helps.
// discussion
Comments