TanStack Query cheatsheet.
Setup
npm i @tanstack/react-query
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const qc = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
<QueryClientProvider client={qc}>
<App />
</QueryClientProvider>
useQuery
const { data, isLoading, error } = useQuery({
queryKey: ["users", id],
queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
});
if (isLoading) return <p>loading</p>;
if (error) return <p>{(error as Error).message}</p>;
return <p>{data.name}</p>;
queryKey conventions
["users"] // list
["users", { page: 1, q: "x" }] // filtered list
["users", id] // detail
["users", id, "posts"] // nested
Stable JSON-serializable keys. Use object for filters.
Suspense mode
const { data } = useSuspenseQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
});
// data is non-nullable; component suspends until ready
Wrap with <Suspense> and <ErrorBoundary>.
useMutation
const m = useMutation({
mutationFn: (data: NewUser) => fetch("/api/users", {
method: "POST",
body: JSON.stringify(data),
}).then(r => r.json()),
onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }),
});
m.mutate({ name: "A" });
Optimistic updates
const m = useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await qc.cancelQueries({ queryKey: ["users", newUser.id] });
const prev = qc.getQueryData(["users", newUser.id]);
qc.setQueryData(["users", newUser.id], newUser);
return { prev }; // context for rollback
},
onError: (_err, _new, ctx) => {
if (ctx?.prev) qc.setQueryData(["users", _new.id], ctx.prev);
},
onSettled: (_, _e, vars) => {
qc.invalidateQueries({ queryKey: ["users", vars.id] });
},
});
Invalidation
qc.invalidateQueries({ queryKey: ["users"] });
qc.invalidateQueries({ queryKey: ["users", id] });
qc.invalidateQueries({ predicate: (q) => q.queryKey[0] === "users" });
qc.invalidateQueries({ exact: true }); // only exact key, no children
Dependent queries
const user = useQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
});
const posts = useQuery({
queryKey: ["posts", user.data?.id],
queryFn: () => fetchPosts(user.data!.id),
enabled: !!user.data,
});
useInfiniteQuery
const q = useInfiniteQuery({
queryKey: ["feed"],
queryFn: ({ pageParam = 0 }) => fetchFeed({ cursor: pageParam }),
getNextPageParam: (last) => last.nextCursor,
initialPageParam: 0,
});
q.data?.pages.flatMap(p => p.items);
q.fetchNextPage();
q.hasNextPage;
q.isFetchingNextPage;
Pagination
const [page, setPage] = useState(0);
const q = useQuery({
queryKey: ["users", page],
queryFn: () => fetchUsers(page),
placeholderData: keepPreviousData,
});
keepPreviousData keeps the last page visible while next loads.
Polling
useQuery({
queryKey: ["jobs"],
queryFn: fetchJobs,
refetchInterval: 5000,
refetchIntervalInBackground: false,
});
Manual cache
qc.setQueryData(["user", id], updated);
qc.getQueryData(["user", id]);
qc.removeQueries({ queryKey: ["user", id] });
qc.resetQueries();
Prefetch
await qc.prefetchQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
});
Prime cache before navigating to a route, etc.
useQueries
const queries = useQueries({
queries: ids.map((id) => ({
queryKey: ["user", id],
queryFn: () => fetchUser(id),
})),
});
Parallel queries.
Typing
function fetchUser(id: number): Promise<User> { ... }
const q = useQuery({
queryKey: ["user", id] as const,
queryFn: () => fetchUser(id),
});
q.data; // User | undefined
ZodFetcher pattern
function makeQuery<T>(
schema: z.ZodSchema<T>,
key: unknown[],
url: string,
) {
return useQuery({
queryKey: key,
queryFn: async () => schema.parse(await (await fetch(url)).json()),
});
}
Devtools
npm i -D @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
<QueryClientProvider client={qc}>
<App />
<ReactQueryDevtools />
</QueryClientProvider>
Common mistakes
- Using mutable objects in queryKey — cache misses.
staleTime: 0(default!) — refetches everywhere. Increase to 30s+.- Not invalidating after mutation — stale list.
- Mutating data directly inside queryFn — defeats cache.
- Disabling refetchOnMount everywhere — stale stays stale.
Read this next
If you want my Query + Zod setup, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .