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 .