TanStack Query v5 Reference: useQuery, useMutation, Cache Invalidation & Next.js
TanStack Query v5 (formerly React Query) manages server state: fetching, caching, synchronising, and updating remote data. The core insight is that server state is fundamentally different from client state — it can become stale, needs background refreshing, and can be shared across components. You stop writing loading/error/data state manually and start thinking in query keys. V5 breaking change: callbacks (onSuccess, onError, onSettled) were removed from useQuery — use useEffect or mutation callbacks instead.
1. Setup & useQuery
QueryClient, QueryClientProvider, useQuery with query keys, and status handling
// npm install @tanstack/react-query @tanstack/react-query-devtools
// main.tsx / app/providers.tsx:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute before data is considered stale
gcTime: 5 * 60 * 1000, // 5 minutes before unused cache is garbage collected
retry: 3, // retry failed requests 3 times
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
refetchOnWindowFocus: true, // refetch when tab is focused
},
},
});
export function Providers({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// useQuery:
import { useQuery } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, isError, error, isFetching } = useQuery({
queryKey: ["users", userId], // cache key — array, first element = namespace
queryFn: () => fetch(`/api/users/${userId}`).then(res => {
if (!res.ok) throw new Error("Failed to fetch");
return res.json() as Promise<User>;
}),
enabled: !!userId, // don't fetch if userId is undefined
staleTime: 5 * 60 * 1000, // override default — 5min for user data
select: (data) => data.user, // transform data before component receives it
});
if (isLoading) return <Spinner />;
if (isError) return <p>Error: {error.message}</p>;
// isFetching is true during background refetches (data is still shown)
return <div>{data.name} {isFetching && <LoadingDot />}</div>;
}
2. useMutation & Cache Invalidation
useMutation for writes, invalidateQueries to refetch, and optimistic updates
import { useMutation, useQueryClient } from "@tanstack/react-query";
function CreateUserForm() {
const queryClient = useQueryClient();
const createUser = useMutation({
mutationFn: (newUser: { name: string; email: string }) =>
fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
}).then(res => res.json()),
// Invalidate after mutation — triggers refetch of users list:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
// Optimistic update pattern:
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ["users"] }); // cancel in-flight refetches
const prev = queryClient.getQueryData(["users"]); // snapshot
queryClient.setQueryData(["users"], (old: User[]) => [
...(old ?? []),
{ ...newUser, id: "temp-" + Date.now() },
]);
return { prev }; // return context for rollback
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(["users"], context?.prev); // rollback
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] }); // always refetch after
},
});
return (
<button
onClick={() => createUser.mutate({ name: "Alice", email: "alice@example.com" })}
disabled={createUser.isPending}
>
{createUser.isPending ? "Creating..." : "Create User"}
</button>
);
}
3. Query Keys, Dependent Queries & Pagination
Query key factories, dependent queries, paginated and infinite scroll patterns
// Query key factory — consistent keys across the app:
const userKeys = {
all: () => ["users"] as const,
lists: () => [...userKeys.all(), "list"] as const,
list: (filters: F) => [...userKeys.lists(), { filters }] as const,
detail: (id: string) => [...userKeys.all(), id] as const,
};
// Use:
queryClient.invalidateQueries({ queryKey: userKeys.lists() }); // all list queries
queryClient.invalidateQueries({ queryKey: userKeys.detail("1") }); // specific user
// Dependent query — only fetch comments after user is loaded:
const { data: user } = useQuery({ queryKey: userKeys.detail(userId), queryFn: fetchUser });
const { data: comments } = useQuery({
queryKey: ["comments", user?.id],
queryFn: () => fetchComments(user!.id),
enabled: !!user?.id, // waits until user is loaded
});
// Paginated query (useQuery — replaces data on page change):
const [page, setPage] = useState(1);
const { data, isPlaceholderData } = useQuery({
queryKey: ["users", "list", { page }],
queryFn: () => fetchUsers(page),
placeholderData: keepPreviousData, // show old data while fetching next page
});
// Infinite scroll (useInfiniteQuery):
import { useInfiniteQuery } from "@tanstack/react-query";
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["users", "infinite"],
queryFn: ({ pageParam }) => fetchUsers(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => lastPage.hasMore ? pages.length + 1 : undefined,
});
// data.pages is array of pages; data.pages.flat() = all items
4. Prefetching, Suspense & Server State
Prefetch on hover, React Suspense integration, and Next.js server prefetching
// Prefetch on hover — pre-warm cache before user navigates:
function UserLink({ userId }: { userId: string }) {
const queryClient = useQueryClient();
return (
<a
href={`/users/${userId}`}
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
staleTime: 10 * 60 * 1000, // don't refetch if already fresh
});
}}
>
View Profile
</a>
);
}
// Suspense mode — let React Suspense handle loading state:
const { data } = useSuspenseQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
});
// No isLoading check needed — component suspends until data is ready
// Wrap with: <Suspense fallback={<Spinner />}><UserProfile /></Suspense>
// Next.js App Router — prefetch on server, hydrate on client:
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
export default async function UsersPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ["users"], queryFn: fetchUsers });
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UsersClient /> {/* client component uses useQuery — data already in cache */}
</HydrationBoundary>
);
}
5. DevTools, Error Boundaries & V5 Migration
ReactQueryDevtools, error boundaries per query, and v4 → v5 breaking changes
// ReactQueryDevtools — browser panel showing cache state:
// Show in dev only, floating button bottom-right:
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-right" />
// Query-level error boundary:
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
onReset={reset}
>
<UserProfile userId={userId} />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
// V4 → V5 breaking changes (most common):
// cacheTime → gcTime
// useQuery onSuccess/onError/onSettled callbacks REMOVED — use useEffect or mutation callbacks
// isLoading → isPending (for mutations); isInitialLoading → isLoading && isFetching
// suspense: true → useSuspenseQuery()
// keepPreviousData: true → placeholderData: keepPreviousData
// getNextPageParam receives (lastPage, allPages, lastPageParam, allPageParams)
// Infinite query: initialPageParam required (was optional)
// Useful imperative API:
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.setQueryData(["users", userId], updater);
queryClient.removeQueries({ queryKey: ["users"] });
queryClient.cancelQueries({ queryKey: ["users"] });
const cached = queryClient.getQueryData(["users", userId]);
Track Node.js and React ecosystem releases at ReleaseRun. Related: React Reference | Next.js App Router Reference | tRPC Reference | React EOL Tracker
🔍 Free tool: npm Package Health Checker — check @tanstack/react-query and related packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com