Data Fetching with TanStack Query
TanStack Query (formerly React Query) is the de-facto standard for server data in React. Instead of hand-rolling loading flags and caches in every useEffect , you describe what you want with a query key and a fetch function — and the library handles caching, deduplication, background refetching, retries, and stale data for you. This lesson shows the useQuery / useMutation workflow you'll use in real apps.
Learn Data Fetching with TanStack Query in our free React course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a…
Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
1️⃣ The Pain It Removes
A hand-written fetch needs three pieces of state, a cleanup guard against race conditions, and it re-fetches from scratch every mount with no shared cache:
TanStack Query gives you a tidy status machine instead of three booleans. The demo models those status transitions:
2️⃣ Setup + Your First useQuery
Wrap your app once in a QueryClientProvider , then call useQuery anywhere:
The query key is the cache identity. Ask for the same key twice and you get one fetch, shared. Your turn — make the cache return the stored value instead of re-fetching:
3️⃣ Dynamic Keys & Dependent Queries
Put variables in the key so each one caches separately. Use enabled to wait for prerequisites:
4️⃣ Changing Data with useMutation
Reads use useQuery ; writes use useMutation . After a successful write, invalidate the affected queries so they refetch and the UI updates:
The demo models that invalidate-then-refetch cycle so you can see fresh data come back after a change:
These lines of a useQuery call are scrambled. Put them in the correct order:
1) Two components both call useQuery({' queryKey: ["todos"] '}) . How many network requests fire?
One. The shared query key means both read the same cache entry and the request is deduped.
2) You just POSTed a new todo. What do you call so the list updates?
queryClient.invalidateQueries({' queryKey: ["todos"] '}) in the mutation's onSuccess — it marks the query stale so it refetches.
It pauses the query — it won't fetch until enabled becomes truthy. Great for dependent queries that need a value first.
📋 Quick Reference
Model query deduplication. Three components request the same key; only the first should hit the network. Complete the cache so repeated keys are served without fetching.
Practice quiz
What does TanStack Query (formerly React Query) give you from a single useQuery call?
- Only a fetch wrapper
- A global state store like Redux
- Caching by key, request deduplication, status tracking, retries, and background refetching of stale data
- Server-side rendering
Answer: Caching by key, request deduplication, status tracking, retries, and background refetching of stale data. It replaces hand-written loading/error flags and caching with one hook that caches, dedupes, tracks status, retries, and refetches automatically.
What is a query key?
- An array that uniquely identifies a piece of server data and serves as the cache key
- A secret API token
- The HTTP method used
- The component's display name
Answer: An array that uniquely identifies a piece of server data and serves as the cache key. A query key like ['user', userId] identifies the cached data. Two components with the same key share one cached result and one request.
Two components both call useQuery({ queryKey: ['todos'] }). How many network requests fire?
- Two
- Zero
- One per render
- One — the request is deduped via the shared key
Answer: One — the request is deduped via the shared key. The shared query key means both read the same cache entry, so the request is deduplicated into a single fetch.
What does staleTime control?
- How long unused data stays cached before garbage collection
- How long fetched data is considered fresh, during which it won't refetch on mount or focus
- The request timeout
- How many retries happen
Answer: How long fetched data is considered fresh, during which it won't refetch on mount or focus. While data is within staleTime it's 'fresh' and won't refetch on remount/focus. The default is 0, meaning data is immediately stale.
What is gcTime (formerly cacheTime)?
- How long unused data remains in the cache after the last component using it unmounts, before being discarded
- How long data stays fresh
- The time before the first fetch
- The retry delay
Answer: How long unused data remains in the cache after the last component using it unmounts, before being discarded. gcTime is the garbage-collection window for inactive queries — distinct from staleTime, which governs freshness while data is in use.
When do you use useMutation instead of useQuery?
- For reading data (GET-like)
- Only for paginated reads
- For changing data (POST/PUT/DELETE-like)
- Never; useQuery handles writes too
Answer: For changing data (POST/PUT/DELETE-like). useQuery reads data; useMutation changes it. After a successful mutation you typically invalidate related queries so they refetch.
After a successful POST, how do you make the affected list update?
- Call useEffect to refetch manually
- todos
Answer: todos. invalidateQueries marks matching queries stale so the library refetches them and the UI reflects the change — no manual useEffect needed.
What does enabled: false do on a query?
- Deletes the cached data
- Disables caching
- Forces an immediate fetch
- Pauses the query so it won't fetch until enabled becomes truthy
Answer: Pauses the query so it won't fetch until enabled becomes truthy. It gates the query — great for dependent queries, e.g. enabled: !!user?.teamId waits until the prerequisite value exists.
Why might a query never enter its error state on an HTTP 404?
- TanStack Query ignores 404s by design
- fetch only rejects on network failure, so you must check res.ok and throw in the queryFn
- 404 is treated as success data
- You forgot to set retries
Answer: fetch only rejects on network failure, so you must check res.ok and throw in the queryFn. fetch resolves on HTTP errors and only rejects on network failure. Check res.ok and throw so the queryFn signals an error.
What must wrap your app so useQuery works?
- BrowserRouter
- StrictMode
- QueryClientProvider with a QueryClient, set up once at the root
- Suspense
Answer: QueryClientProvider with a QueryClient, set up once at the root. Without a QueryClientProvider at the root you get a 'No QueryClient set' error. Create one QueryClient and provide it once.