Home/Learn/React/TanStack Query — Queries, Keys and Caching

TanStack Query — Queries, Keys and Caching

Intermediate
Data

Server data is not application state. A query library treats it as a cache of someone else's data, which is what removes the loading flags, the races and the duplicate requests.

Overview

The insight behind TanStack Query is a distinction: the state you own — a modal being open, a draft being typed — behaves differently from data that lives on a server and can change without you. Storing server data in useState means manually maintaining a stale copy, and every feature after that is bookkeeping. A query library caches by key, deduplicates concurrent requests, refetches when data goes stale, and gives every consumer the same entry. In interviews this is the expected answer to "how do you fetch data in React", and being able to say why is the point.

A Query

A key, a function, and every state you were tracking by hand.

The key is the identity of the data
const { data, isLoading, isFetching, isError, error, refetch } = useQuery({
  queryKey: ['problems', { topic, page }],
  queryFn: ({ signal }) => listProblems({ topic, page, signal }),
  staleTime: 60_000,        // treat as fresh for a minute
})

if (isLoading) return <ProblemsSkeleton />
if (isError)   return <ErrorState error={error} onRetry={refetch} />
return <ProblemList problems={data} />

// What you no longer write: the loading flag, the error state, the
// abort controller, the race guard, the mount check, and the
// deduplication when three components ask for the same thing.

// The key IS the cache identity. Anything the queryFn depends on
// must be in it, or you will show one topic's data under another.
queryKey: ['problems', { topic, page }]     // correct
queryKey: ['problems']                      // wrong — topic ignored

// Changing the key is how you refetch: no manual trigger needed.

Freshness and Garbage Collection

Two timings that people confuse, and the defaults worth changing.

staleTime controls refetching, gcTime controls eviction
staleTime  // how long data is considered FRESH. While fresh, no
           // refetch happens at all. Default: 0 — refetch on every mount.
gcTime     // how long an UNUSED cache entry is kept before deletion.
           // Default: 5 minutes.

// Sensible defaults for a content site
const client = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      retry: 2,
      refetchOnWindowFocus: false,   // on by default; often surprising
    },
  },
})

// The lifecycle: fresh -> stale -> (refetched on mount/focus/reconnect)
//                unused -> garbage collected after gcTime

// Stale data is still SHOWN. A refetch happens in the background and
// the screen updates when it lands — so the user never sees a blank
// page for data you already have. That is the whole appeal.

Dependent and Parallel Queries

Ordering requests without building a waterfall by accident.

enabled, useQueries, and prefetch on hover
// Dependent — wait for the first result
const { data: user } = useQuery({ queryKey: ['me'], queryFn: getMe })
const { data: progress } = useQuery({
  queryKey: ['progress', user?.id],
  queryFn: () => getProgress(user.id),
  enabled: !!user?.id,              // does not run until the id exists
})

// Parallel — independent, so both start immediately
const results = useQueries({
  queries: topics.map(t => ({
    queryKey: ['problems', t],
    queryFn: () => listProblems({ topic: t }),
  })),
})

// Prefetch on intent — the page feels instant when the user arrives
<Link
  to={`/problems/${slug}`}
  onMouseEnter={() => queryClient.prefetchQuery({
    queryKey: ['problem', slug],
    queryFn: () => getProblem(slug),
  })}
/>

Key Points to Remember

  • 1Server data is a cache of someone else's state, not application state — that distinction is the whole library
  • 2The query key is the cache identity; every input the query function uses must appear in it
  • 3staleTime decides when a refetch happens, gcTime decides when an unused entry is deleted
  • 4Stale data stays on screen while it refetches in the background, so the user rarely sees a blank state
  • 5enabled sequences dependent queries, and prefetching on hover makes navigation feel instant

Interview Questions

Sign in to ask Aria
1

Why treat server data differently from client state?

Medium
2

What happens if you leave a query parameter out of the query key?

Medium
3

What is the difference between staleTime and gcTime?

Hard

Ask Aria about TanStack Query — Queries, Keys and Caching

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…