C.W.K.
Stream
Lesson 06 of 07 · published

TanStack Query — A Brief Touchpoint

~11 min · tanstack-query, state-management, ecosystem

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
TanStack Query is the server-state library everyone reaches for once their app has more than one component sharing the same fetched data. Knowing what it solves — and where the line between 'use it' and 'roll your own' sits — is enough for this quest.

What TanStack Query is

A library that wraps your fetcher and adds a cache, deduplication of concurrent requests, background refetching, stale-while-revalidate freshness, retry logic, and a Suspense-compatible API. You write the fetcher function; the library handles operational concerns.

The shape

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});

Multiple components can call useQuery with the same queryKey — only one fetch fires, all components get the same data, the cache stays consistent.

Suspense mode

Add suspense: true and the hook becomes use()-shaped: it suspends instead of returning isLoading. Combined with Suspense + error boundaries, your component body is just the success path. Best of both worlds: the cache benefits of TanStack Query + the declarative loading/error of Suspense.

When to reach for it

  • Two or more components fetch the same data (sidebar + main panel both need the user object).
  • You need optimistic updates, retries, refetching on window focus, polling, infinite scroll.
  • You have a list page → detail page navigation and want the list cache to populate the detail without re-fetching.

When you don't need it

  • One-off fetches with no sharing or revalidation concerns.
  • Server-driven state that changes via WebSocket / SSE (you'll want a real-time-shaped library or a custom hook instead).
  • Pure SPA bootstrapping where the data is module-level constant.
Use the library when the operational concerns are the real work. TanStack Query isn't 'fetch with extra steps' — it's the abstraction over the cache, retry, dedup, refetch behaviors that you'd otherwise reimplement (badly) across hooks. The moment you start writing your own cache map, switch.

Code

Basic useQuery with Suspense mode·tsx
import { QueryClient, QueryClientProvider, useSuspenseQuery } from "@tanstack/react-query";
import { Suspense } from "react";

const queryClient = new QueryClient();

function User({ userId }: { userId: string }) {
  // Suspense-compatible — no isLoading/error in the destructure.
  const { data: user } = useSuspenseQuery({
    queryKey: ["user", userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
  });
  return <p>{user.name}</p>;
}

export function App({ userId }: { userId: string }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<p>Loading…</p>}>
        <User userId={userId} />
      </Suspense>
    </QueryClientProvider>
  );
}
Mutation with optimistic update — the TanStack Query strength·tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";

function useRenameConversation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, title }: { id: string; title: string }) =>
      fetch(`/api/conversations/${id}/title`, {
        method: "PATCH",
        body: JSON.stringify({ title }),
      }).then((r) => r.json()),
    onMutate: async ({ id, title }) => {
      // Optimistic update — UI shows new title immediately.
      await queryClient.cancelQueries({ queryKey: ["conversation", id] });
      const previous = queryClient.getQueryData(["conversation", id]);
      queryClient.setQueryData(["conversation", id], (old: { title: string }) => ({
        ...old,
        title,
      }));
      return { previous, id };
    },
    onError: (_e, _vars, ctx) => {
      // Roll back on failure.
      if (ctx) queryClient.setQueryData(["conversation", ctx.id], ctx.previous);
    },
    onSettled: (_d, _e, { id }) => {
      // Re-sync with server.
      queryClient.invalidateQueries({ queryKey: ["conversation", id] });
    },
  });
}

External links

Exercise

Install @tanstack/react-query. Set up a QueryClient at your app root. Refactor a component that fetches with useEffect + useState into one that uses useSuspenseQuery. Verify: (1) the component is shorter; (2) two instances of the component with the same query key only trigger one network request; (3) navigating away and back doesn't re-fetch (within the default stale time).
Hint
Open Network panel in DevTools. With useState pattern, every render with new userId triggers a fetch. With useSuspenseQuery, the first fetch caches; subsequent visits with the same key hit cache.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.