React Query (TanStack Query) Cheat Sheet
A reference for TanStack Query's useQuery and useMutation hooks, cache keys, and invalidation strategies for server-state management.
QueryClient Setup
Providing a QueryClient to the React tree.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';const queryClient = new QueryClient();function App() { return ( <QueryClientProvider client={queryClient}> <Todos /> </QueryClientProvider> );}
useQuery
Fetching and caching server data declaratively.
import { useQuery } from '@tanstack/react-query';function Todos() { const { data, isPending, isError, error } = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then((res) => res.json()), staleTime: 60_000, // considered fresh for 60s }); if (isPending) return <p>Loading...</p>; if (isError) return <p>Error: {error.message}</p>; return ( <ul> {data.map((t) => ( <li key={t.id}>{t.title}</li> ))} </ul> );}
useMutation
Sending writes and invalidating related queries on success.
import { useMutation, useQueryClient } from '@tanstack/react-query';function AddTodo() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); return ( <button onClick={() => mutation.mutate({ title: 'New todo' })}> Add Todo </button> );}
Core Concepts
Key options and hooks you will reach for repeatedly.
- queryKey- an array that uniquely identifies a query, used for caching and invalidation
- queryFn- the async function that fetches and returns the data
- staleTime- how long data is considered fresh before a background refetch is triggered
- gcTime- how long unused/inactive cached data is kept before garbage collection (formerly cacheTime)
- invalidateQueries- marks matching queries as stale and triggers a refetch
- useQueryClient- hook to access the QueryClient instance for manual cache reads/writes
- enabled- option to conditionally prevent a query from running automatically
- useInfiniteQuery- hook for paginated or infinite-scroll data fetching
Server Prefetching & Hydration
Prefetch a query on the server, dehydrate the cache, and hydrate it on the client so the first render already has data with no loading flash.
// server (e.g. a framework loader or RSC)import { QueryClient, dehydrate } from '@tanstack/react-query';async function loadPage() { const queryClient = new QueryClient(); await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); return { dehydratedState: dehydrate(queryClient) };}// clientimport { HydrationBoundary } from '@tanstack/react-query';function Page({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Todos /> </HydrationBoundary> );}
Dependent Queries & Infinite Pagination
Gate a query on data from another query, and paginate with useInfiniteQuery's cursor-based page params.
// dependent query: only runs once `userId` is knownconst { data: user } = useQuery({ queryKey: ['me'], queryFn: fetchMe });const { data: projects } = useQuery({ queryKey: ['projects', user?.id], queryFn: () => fetchProjects(user.id), enabled: !!user?.id,});// cursor-based infinite paginationimport { useInfiniteQuery } from '@tanstack/react-query';const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }), initialPageParam: null, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,});
select, placeholderData & Query Cancellation
Transform cached data without extra re-renders, keep old data visible during refetch, and cancel in-flight requests via the queryFn's AbortSignal.
import { keepPreviousData, useQuery } from '@tanstack/react-query';const { data: titles } = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then((r) => r.json()), // only re-renders when the derived value actually changes select: (todos) => todos.map((t) => t.title),});const { data: page } = useQuery({ queryKey: ['items', pageIndex], queryFn: ({ signal }) => fetch(`/api/items?page=${pageIndex}`, { signal }).then((r) => r.json()), // keep showing the previous page's data while the next page loads placeholderData: keepPreviousData,});
queryOptions() Factory & useSuspenseQuery
Centralize a query's key, fn, and options in one typed, reusable object, and opt into Suspense-driven loading with useSuspenseQuery.
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';// reusable, type-safe, referenced from components, prefetch calls, and loaders alikefunction todoOptions(id) { return queryOptions({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), staleTime: 30_000, });}function Todo({ id }) { // suspends the component instead of returning isPending; data is never undefined const { data } = useSuspenseQuery(todoOptions(id)); return <p>{data.title}</p>;}// same options object reused for imperative prefetchqueryClient.prefetchQuery(todoOptions(id));
Advanced Options & Utilities
Configuration knobs and helper hooks that matter in production apps beyond the basic useQuery call.
- retry / retryDelay- number of retries (or a function) and the backoff delay function applied after a failed query
- refetchOnWindowFocus- controls whether a stale query automatically refetches when the browser tab regains focus
- networkMode- 'online' (default), 'always', or 'offlineFirst' -- governs whether queries fire while offline
- queryClient.ensureQueryData- returns cached data if fresh, otherwise fetches and caches it; ideal inside route loaders
- structuralSharing- default optimization that keeps unchanged parts of a query result referentially stable across refetches
- meta- arbitrary metadata attached to a query/mutation, readable in global callbacks like onError
- useIsFetching / useIsMutating- hooks that report in-flight query/mutation counts, useful for a global loading indicator
- QueryErrorResetBoundary- pairs with useSuspenseQuery + an ErrorBoundary to let a 'retry' button reset a thrown query error
For optimistic updates, write the new value with queryClient.setQueryData inside onMutate, save the previous value from context, and roll it back in onError -- this makes the UI feel instant without waiting for the server round-trip.