Server data in React: fetching, caching, refetching and writing it back.
From what a server and an API are to running a cache in production. You start with plain fetch in a React component, see why it gets hard, then learn TanStack Query one idea at a time on a book catalogue app: queries and keys, freshness and refetching, mutations and invalidation, optimistic updates, infinite lists, Suspense, testing, server rendering and offline use.
What you’ll be able to do
Explain how a web app talks to a server: requests, methods, status codes, JSON and promises.
Set up TanStack Query and write queries whose keys keep the right data for the right inputs.
Choose staleTime, gcTime and refetch triggers deliberately, and predict when a request will happen.
Write mutations that keep every screen up to date with invalidation, cache writes or optimistic updates that roll back.
Build dependent, parallel, paginated and infinite queries, with prefetching and Suspense, without waterfalls.
Test, debug and structure queries at scale, including server rendering, hydration and offline use.
Modules
Servers, APIs and JSON· 9 slides
Asking from code: fetch and promises· 10 slides
Fetching inside a React component· 9 slides
Why fetching gets hard, and what a cache is· 8 slides
Setting up and your first query· 10 slides
Query keys· 10 slides
Loading, errors and retries· 10 slides
Freshness, refetching and garbage collection· 11 slides
Parallel, dependent and shaped queries· 9 slides
Mutations: writing to the server· 9 slides
Invalidation and writing to the cache· 9 slides
Optimistic updates· 10 slides
Pagination and infinite lists· 9 slides
Prefetching, Suspense and error boundaries· 8 slides
Testing queries and mutations· 8 slides
Inside the cache· 9 slides
Server rendering and hydration· 9 slides
Offline, network modes and persistence· 8 slides
Query options and key factories at scale· 9 slides
Mastery challenge· 9 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: Servers, APIs and JSON
TanStack Query1 / 183
Module 1 · Servers, APIs and JSON
This course builds one app: a book catalogue
TanStack Query is a tool for one job: showing data that lives somewhere else, and keeping it up to date. To learn it from nothing, you will follow one small app from the first module to the last: a book catalogue.
A list of books, with a search box and pages
A page for each book, with its details
Reviews under each book, and a form to post your own
A reading list you can add books to and remove them from
None of the books are in the app itself. They live on another computer, and the app has to ask for them. Before any TanStack Query, this module explains how that asking works.
TanStack Query2 / 183
Module 1 · Servers, APIs and JSON
Your app asks; a server answers
The app runs in a browser on someone’s laptop or phone. The books are stored on a server: a computer, somewhere on the internet, whose job is to answer questions. The app is the client: the side that asks. They talk over the network, and every question takes time to travel there and back.
One question, one answer. The time between them is never zero.
Client
The program that asks for data: here, the app in the browser.
Server
A computer that keeps data and answers requests for it.
Network
The connection between them. It is slow compared with the computer itself, and it can fail.
TanStack Query3 / 183
Module 1 · Servers, APIs and JSON
An API is the list of questions a server accepts
A server can’t answer just any question. It publishes an API (application programming interface): a fixed set of requests it understands, like a menu in a restaurant. Each item on the menu is an endpoint: an address, plus what you want to do there.
Request
What it does
GET /books
List the books
GET /books?search=dune
List the books that match "dune"
GET /books/42
The details of book 42
GET /books/42/reviews
The reviews of book 42
POST /books/42/reviews
Add a review to book 42
GET /reading-list
Your reading list
DELETE /reading-list/42
Take book 42 off your reading list
QuizInteractive · disabled in the PDF
The app wants to show the details of book 7. Which request fits?
The app wants to show the details of book 7. Which request fits?
TanStack Query4 / 183
Module 1 · Servers, APIs and JSON
A URL says where to ask, and with which details
Every request goes to a URL (the address you see in a browser’s bar). Its parts each have a job.
Scheme: https means the conversation is encrypted.
Host: which server to ask.
Path: what you are asking about: /books, /books/42.
Query string: optional details after ?, as name=value pairs joined by &. Here: search for "dune", and give me the second page.
QuizInteractive · disabled in the PDF
In /books?search=tolkien&page=3, what does page=3 do?
In /books?search=tolkien&page=3, what does page=3 do?
TanStack Query5 / 183
Module 1 · Servers, APIs and JSON
The method says what you want to do
The same address can mean different things depending on the request’s method (also called the HTTP verb). HTTP is the language browsers and servers speak.
Method
Means
Example
GET
Read. Changes nothing.
Get the list of books
POST
Create something new
Post a review
PUT / PATCH
Replace / change part of something
Edit your review
DELETE
Remove something
Remove a book from your reading list
Reads and writes.GET only reads, so it is safe to repeat as often as you like. The others write: they change what the server stores. That difference is the heart of TanStack Query, which handles reads as queries and writes as mutations.
QuizInteractive · disabled in the PDF
You fill in a form and press "Post review". Which method does the request use?
You fill in a form and press "Post review". Which method does the request use?
TanStack Query6 / 183
Module 1 · Servers, APIs and JSON
Every answer starts with a status code
The server’s answer is a response. Its first line is a three-digit status code: a quick verdict on how the request went, before any data.
Code
Means
200 OK
Here is what you asked for
201 Created
Your new thing was saved
204 No Content
Done, and there is nothing to send back
400 Bad Request
Your request was wrong (a missing field, say)
401 Unauthorized
You need to sign in first
404 Not Found
There is nothing at that address
500 Server Error
The server broke while answering
The rule of thumb: 2xx went well, 4xx the client asked for something wrong, 5xx the server failed. A 4xx is usually worth showing to the person; a 5xx is often worth trying again.
QuizInteractive · disabled in the PDF
The app asks for GET /books/99999 and that book doesn’t exist. Which status code should come back?
The app asks for GET /books/99999 and that book doesn’t exist. Which status code should come back?
TanStack Query7 / 183
Module 1 · Servers, APIs and JSON
Data travels as JSON
After the status, a response usually has a body: the data itself. APIs nearly always send it as JSON (JavaScript Object Notation), plain text that looks like JavaScript objects and lists.
GET /books/42
1{2"id":42,3"title":"The Left Hand of Darkness",4"author":"Ursula K. Le Guin",5"year":1969,6"tags":["science fiction","classic"],7"available":true8}
{ } is an object: named fields, each a "name": value pair.
[ ] is an array: a list of values in order.
Values are strings in double quotes, numbers, true or false, null, or more objects and arrays.
QuizInteractive · disabled in the PDF
In the JSON above, what is the value of tags?
In the JSON above, what is the value of tags?
TanStack Query8 / 183
Module 1 · Servers, APIs and JSON
A whole exchange, start to finish
Put together, a request is a method and a URL (plus, for writes, a body), and a response is a status code and a body. Here is posting a review, as the two sides actually send it.
Content-Type is a header: a label on the request or response saying what the body is.
Put in orderInteractive · disabled in the PDF
Put loading the book list in order.
1.The server answers 200 with a JSON array
2.The request travels over the network
3.The server looks up the books
4.The app turns the JSON into a list on screen
5.The app sends GET /books to the server
TanStack Query9 / 183
Module 1 · Servers, APIs and JSON
Recap
The app is a client; the books live on a server; they talk over a slow, fallible network.
An API is the set of requests a server accepts; each is a method plus a URL.
GET reads; POST, PUT, PATCH and DELETE write.
A response has a status code (2xx fine, 4xx client’s mistake, 5xx server’s failure) and usually a JSON body.
FlashcardsInteractive · disabled in the PDF
What is a server?
A computer that stores data and answers requests for it.
What is an API?
The set of requests a server accepts, each an endpoint: a method and an address.
What is a query string?
The ?name=value&… part of a URL: extra details for the request.
Which method only reads?
GET. It must never change anything on the server.
What does a 404 mean?
Not found: nothing exists at that address.
What is JSON?
Plain text for data, with objects in { } and arrays in [ ].
What is a header?
A label on a request or response, such as Content-Type: application/json.
Module 2: Asking from code: fetch and promises
TanStack Query10 / 183
Module 2 · Asking from code: fetch and promises
Code can’t stop and wait for the network
Code normally runs one line after another, each finishing before the next. A network request might take a second, or ten. If the browser stopped on that line, the whole page would freeze: no scrolling, no typing, no clicks.
So JavaScript does slow things asynchronously: it starts the request, carries on with other work, and comes back to it when the answer arrives. The code that runs "later" has to be written in a particular way, which is what this module is about.
Synchronous
Each step finishes before the next one starts.
Asynchronous
A step starts now and finishes later, while other work carries on.
TanStack Query11 / 183
Module 2 · Asking from code: fetch and promises
A promise is a receipt for an answer that hasn’t arrived
When you start something slow, JavaScript hands you a promise at once: an object standing for a value you don’t have yet, like the ticket a coat check gives you. Later it settles, one of two ways.
A promise settles once, and then never changes.
Promise
An object for a value that will arrive later, or an error instead.
Fulfilled
The promise settled with a value.
Rejected
The promise settled with an error.
TanStack Query12 / 183
Module 2 · Asking from code: fetch and promises
fetch sends a request and returns a promise
fetch is built into every browser. Give it a URL and it sends a GET. It returns a promise of a Response. The body arrives separately, so reading it as JSON with .json() is a second promise.
.then(fn) says: when this promise fulfils, run fn with its value.
QuizInteractive · disabled in the PDF
What appears in the console first?
What appears in the console first?
TanStack Query13 / 183
Module 2 · Asking from code: fetch and promises
async and await read like ordinary code
Chains of .then get hard to read. Inside a function marked async, you can write await in front of a promise: the function pauses at that line until the promise settles, while the rest of the page keeps running.
Lines 1:async marks a function that can pause. It always returns a promise.
Lines 2-4: Send the request and wait for the response to start arriving.
Lines 5: Wait again, this time for the body, read as JSON.
Lines 6: Returning a value fulfils the function’s promise with it.
Lines 9: Calling it doesn’t give you the books: it gives you a promise of them.
Remember this shape. A function that returns a promise of data is exactly what TanStack Query asks you for. It calls it a query function.
PlaygroundInteractive · disabled in the PDF
TypeScript
1// A pause that stands in for the network (there is none here)2constwait=(ms:number)=>3newPromise((done)=>setTimeout(done, ms))45asyncfunctiongetBooks(){6awaitwait(500)7return[{title:'Dune'},{title:'Emma'}]8}910const promise =getBooks()11console.log('asked; got', promise)12promise.then((books)=> console.log('books:', books.length))13console.log('the page carries on')
Predict the order of the three lines, then run it. Try await wait(2000).
TanStack Query14 / 183
Module 2 · Asking from code: fetch and promises
fetch doesn’t fail on a 404
Surprisingly, fetch only rejects when the request couldn’t be made at all (no network, say). A 404 or a 500 is still an answer, so the promise fulfils. You have to look at response.ok, which is true for 2xx codes, and fail yourself.
throw stops the function and rejects its promise with that error.
Find the bugInteractive · disabled in the PDF
Book 99999 doesn’t exist. This function "succeeds" with the server’s error message as if it were a book. Which line is wrong?
TypeScript
TanStack Query15 / 183
Module 2 · Asking from code: fetch and promises
Catching errors with try and catch
When an awaited promise rejects, the error is thrown at that line. Wrap the code in try { … } catch (error) { … } to handle it instead of letting it escape.
TypeScript
1try{2const book =awaitgetBook(42)3 console.log(book.title)4}catch(error){5 console.log('Could not load the book', error)6}
QuizInteractive · disabled in the PDF
Which of these makes fetch itself reject (before you check response.ok)?
Which of these makes fetch itself reject (before you check response.ok)?
TanStack Query16 / 183
Module 2 · Asking from code: fetch and promises
Sending data: method, headers and body
fetch takes a second argument for anything other than a plain GET: the method, headers, and a body. JSON.stringify turns a JavaScript object into JSON text.
TypeScript
1asyncfunctionpostReview(bookId:number, review:NewReview){2const response =awaitfetch(3`https://api.example.com/books/${bookId}/reviews`,4{5method:'POST',6headers:{'Content-Type':'application/json'},7body: JSON.stringify(review)8}9)10if(!response.ok)thrownewError('Could not post the review')11return response.json()12}
Fill the blanksInteractive · disabled in the PDF
Remove a book from the reading list.
TypeScript
1const response =awaitfetch(2`https://api.example.com/reading-list/${bookId}`,3{:''}4)5if(!response.)thrownewError('Could not remove it')
TanStack Query17 / 183
Module 2 · Asking from code: fetch and promises
Try it: send a review
There is no network on this page, so the tests replace fetch with a stand-in that answers what they choose: vi.spyOn(globalThis, 'fetch') swaps the real one for a mock, a fake that also remembers how it was called. Your code calls fetch exactly as it would for real.
Code challengeInteractive · disabled in the PDF
Finish postReview(bookId, review). POST the review as JSON (with a Content-Type: application/json header) to https://api.example.com/books/<bookId>/reviews. If the response isn’t ok, throw an Error; otherwise return the parsed JSON body.
TypeScript
1typeNewReview={ stars:number; text:string}23asyncfunctionpostReview(bookId:number, review:NewReview){4// TODO: send it with fetch, check ok, return the JSON5returnnull6}
TanStack Query18 / 183
Module 2 · Asking from code: fetch and promises
One small helper for every read
Every read does the same three things: fetch, check ok, parse JSON. Put them in one helper. The <T> is a TypeScript type parameter: the caller says what shape of data to expect, such as getJson<Book>(…), and gets it typed.
The rest of the course uses getJson and small functions like getBook.
TypeScript in one sentence. TypeScript is JavaScript with types: labels like number or Book that describe values, so mistakes show up in your editor before the code runs. The typescript course goes further.
TanStack Query19 / 183
Module 2 · Asking from code: fetch and promises
Recap
Slow work is asynchronous: it starts now and finishes later.
A promise stands for a future value; it fulfils with a value or rejects with an error.
fetch returns a promise of a response; .json() is a second promise for the body.
fetch doesn’t reject on 404 or 500: check response.ok and throw.
A function that returns a promise of data is what TanStack Query will call a query function.
FlashcardsInteractive · disabled in the PDF
What does fetch return?
A promise of a Response.
What does await do?
Pauses an async function until a promise settles, then gives its value (or throws its error).
When does fetch reject?
Only when no response came back at all, such as being offline.
What is response.ok?
true when the status is 200 to 299.
What does JSON.stringify do?
Turns a JavaScript value into JSON text, for a request body.
What is a type parameter like <T>?
A placeholder type the caller fills in, such as getJson<Book>.
Module 3: Fetching inside a React component
TanStack Query20 / 183
Module 3 · Fetching inside a React component
A component is a function that returns what to show
TanStack Query is used with React, a library for building screens out of components. A component is a function that returns JSX: HTML-like markup that can include JavaScript values in { }. Values passed in from outside are its props.
TSX
1typeBook={ id:number; title:string; author:string}23functionBookList({ books }:{ books:Book[]}){4return(5<ul>6{books.map((book)=>(7<likey={book.id}>8{book.title}, by {book.author}9</li>10))}11</ul>12)13}
map turns each book into a list item; key lets React tell the items apart.
Component
A function that returns what part of the screen should show.
JSX
HTML-like markup inside JavaScript, with values in { }.
Props
The inputs a component is given by the one that uses it.
TanStack Query21 / 183
Module 3 · Fetching inside a React component
State is what a component remembers
useState gives a component a value that survives between renders, and a function to change it. Changing it makes React call the component again (render it) and update the screen. Functions starting with use are hooks: they can only be called at the top of a component.
A component must only describe the screen; it can’t start a request while rendering, or it would start one on every render. useEffect runs code after React has drawn the screen, and again whenever one of its dependencies (the array at the end) has changed.
Lines 2: Start with an empty list, because nothing has arrived yet.
Lines 4: After the first render, the effect runs.
Lines 5: Start the request with the getJson helper from the last module.
Lines 6: When the answer arrives, store it. That renders the component again, now with books.
Lines 8: An empty dependency array: fetch once, when the component appears.
TanStack Query24 / 183
Module 3 · Fetching inside a React component
An empty list and "still loading" look the same
Until the answer arrives, the component shows an empty list, which reads as "no books at all". You need a second piece of state to say that you are still waiting.
If the request rejects, nothing above ever sets loading to false: the page says "Loading…" forever. A third state holds the error.
TSX
1const[books, setBooks]=useState<Book[]>([])2const[loading, setLoading]=useState(true)3const[error, setError]=useState<Error|null>(null)45useEffect(()=>{6getJson<Book[]>('/books')7.then((data)=>setBooks(data))8.catch((err)=>setError(err))9.finally(()=>setLoading(false))10},[])1112if(loading)return<p>Loading…</p>13if(error)return<p>Could not load books.</p>14return<BookListbooks={books}/>
.catch runs on rejection; .finally runs either way.
Find the bugInteractive · disabled in the PDF
This component sends requests over and over, forever. Which line causes it?
TSX
TanStack Query26 / 183
Module 3 · Fetching inside a React component
Fetching again when the input changes
With a search box, the request depends on what was typed. Put search in the dependencies and the effect runs again each time it changes. encodeURIComponent makes the text safe to put in a URL (spaces, & and so on).
Three pieces of state, an effect, and three branches, for one list. Every other screen (a book’s details, its reviews, the reading list) would repeat all of it. And this version still has real bugs, which the next module uncovers.
QuizInteractive · disabled in the PDF
You open a book’s page, then press Back to the catalogue. What does this Catalogue do?
You open a book’s page, then press Back to the catalogue. What does this Catalogue do?
TanStack Query28 / 183
Module 3 · Fetching inside a React component
Recap
A component returns JSX from props and state; changing state renders it again.
Hooks like useState and useEffect are called at the top of a component.
Requests start in an effect, never during render.
Doing it by hand takes data, loading and error state for every request, and forgets everything when the component goes away.
FlashcardsInteractive · disabled in the PDF
What is a render?
React calling a component to find out what it should show now.
What does useState return?
The current value and a function to change it (which renders again).
When does useEffect run?
After the render, and again after renders where a dependency changed.
What is a hook?
A function starting with use that gives a component a React feature, called at its top level.
Why not fetch during render?
Rendering happens often; each answer would render again and fetch again, forever.
Module 4: Why fetching gets hard, and what a cache is
TanStack Query29 / 183
Module 4 · Why fetching gets hard, and what a cache is
Answers can arrive in the wrong order
You type "du", then "dune". Two requests go out. Nothing says the server answers them in order: the "du" search might be slower. If it lands last, the screen shows results for "du" while the box says "dune". This is a race condition: the result depends on which one wins.
QuizInteractive · disabled in the PDF
With the effect from the last module, which results stay on screen?
With the effect from the last module, which results stay on screen?
TanStack Query30 / 183
Module 4 · Why fetching gets hard, and what a cache is
Fixing the race by hand
An effect can return a cleanup function. React calls it before running the effect again, and when the component goes away. Use it to mark the old request as unwanted.
It works, but it is one more thing to remember in every effect, and the request still runs to the end and wastes the network.
TanStack Query31 / 183
Module 4 · Why fetching gets hard, and what a cache is
Two components, two identical requests
The book page shows the title in the header and the full details below. Each part fetches /books/42 in its own effect, so the same request goes out twice, and the two can even disagree if the book changed in between.
BookPage
HeaderGET /books/42
DetailsGET /books/42
ReviewsGET /books/42/reviews
Lifting the fetch to BookPage and passing props avoids it, but couples every part to its parent.
Deduplication
Noticing that a request is already running and sharing it, instead of sending another.
TanStack Query32 / 183
Module 4 · Why fetching gets hard, and what a cache is
What you fetched starts going out of date at once
The moment the list arrives, it is a copy of what the server had a moment ago. Someone else posts a review; a book becomes unavailable. Your screen doesn’t know. Data that may no longer match the server is called stale.
Leave the tab for an hour and come back: still the old list.
Post a review: the review list on screen doesn’t include it until you reload.
Go back to a page you saw: everything is fetched again from nothing, with a spinner.
QuizInteractive · disabled in the PDF
You post a review with fetch. The Reviews component fetched the list in its effect when the page opened. What does it show now?
You post a review with fetch. The Reviews component fetched the list in its effect when the page opened. What does it show now?
TanStack Query33 / 183
Module 4 · Why fetching gets hard, and what a cache is
Server state is not like other state
The text in a search box belongs to the browser: only it can change it, and it is always right. The list of books belongs to the server: you only hold a copy, others can change the original, and getting it takes time and can fail.
Client state
Server state
Examples
Search text, an open menu, a dark mode toggle
Books, reviews, the reading list
Who owns it
The browser
The server
Can it be wrong?
No: it is the original
Yes: your copy goes stale
Getting it
Instant
Slow, and can fail
Tool
useState and friends
A cache, such as TanStack Query
QuizInteractive · disabled in the PDF
Which are server state?
Which are server state?
TanStack Query34 / 183
Module 4 · Why fetching gets hard, and what a cache is
A cache is a labelled shelf of copies
A cache keeps copies of answers so you can reuse them. Picture a shelf where every answer is stored under a label: "books", "book 42", "reviews of book 42". Any component can read from the shelf, so two that want the same thing share one copy and one request.
A component asks the cache, not the server.
If a copy exists, it shows at once: no spinner.
If the copy might be old, a request runs in the background and the screen updates when it lands.
Only with no copy at all does the person wait.
Stale-while-revalidate: show the copy now, check for a newer one quietly.
Cache
A store of copies of answers, each under a label, ready to reuse.
Stale-while-revalidate
Show the copy you have now, and fetch a fresher one in the background.
TanStack Query35 / 183
Module 4 · Why fetching gets hard, and what a cache is
TanStack Query is that cache, for React
TanStack Query (once called React Query) is a library that manages server state. You tell it how to fetch something and what to call it; it runs the request, stores the answer, shares it, tracks loading and errors, ignores stale answers, and refetches when the copy may be out of date.
It does
Cache answers under labels (keys)
Share one request between components
Track loading and errors
Refetch when data may be stale
Retry failures, and update after writes
It does not
Make the requests itself: you give it a function, often using fetch
Replace useState for client state
Know anything about your API until you tell it
QuizInteractive · disabled in the PDF
Who actually sends the HTTP request when you use TanStack Query?
Who actually sends the HTTP request when you use TanStack Query?
TanStack Query36 / 183
Module 4 · Why fetching gets hard, and what a cache is
Recap
Hand-made fetching suffers races, duplicate requests, and stale screens that don’t update after writes.
Server state is a copy of data someone else owns; client state belongs to the browser.
A cache stores answers under labels and shares them: show the copy, refetch quietly.
TanStack Query is that cache for React. You give it the fetching function; it decides when to call it.
FlashcardsInteractive · disabled in the PDF
What is a race condition?
A bug where the result depends on which of two things finishes first.
What is an effect’s cleanup?
The function an effect returns; React calls it before the next run and when the component goes away.
What is stale data?
A copy that may no longer match what the server has.
Server state vs client state?
Server state is a copy of data the server owns; client state lives only in the browser.
What is deduplication?
Sharing a request that is already running instead of sending the same one again.
What is stale-while-revalidate?
Show the cached copy at once, and fetch a fresh one in the background.
Module 5: Setting up and your first query
TanStack Query37 / 183
Module 5 · Setting up and your first query
Installing the package
Libraries are shared as packages and installed with npm, a tool that comes with Node.js. You run it in a terminal, a window where you type commands, from your project’s folder. It downloads the package and records it in package.json, the file listing what the project depends on.
Terminal
1npminstall@tanstack/react-query
The React version of TanStack Query. Version 5 is the one this course teaches.
Package
Code someone published for others to install and use.
npm
The tool (and registry) that installs JavaScript packages.
Terminal
A text window for running commands such as npm install.
TanStack Query38 / 183
Module 5 · Setting up and your first query
The QueryClient holds the cache
Everything starts with a QueryClient: the object that owns the cache (the shelf from the last module) and decides when to fetch. An app makes one, once, outside any component, so it survives every render.
new QueryClient() uses sensible defaults; later modules tune them.
One client, made once. Never write new QueryClient() inside a component’s body: each render would make a new, empty cache, and parts of the app would end up reading and updating different caches.
TanStack Query39 / 183
Module 5 · Setting up and your first query
The provider makes the client reachable everywhere
React components get shared things from a context: a value an ancestor provides to everything below it. QueryClientProvider puts your client in context, so any component under it can use queries. Wrap the whole app in it.
And for free: the answer is cached, shared with any other component using ['books'], retried on failure, and refetched when you come back to the tab.
TanStack Query43 / 183
Module 5 · Setting up and your first query
Same key, one request
Two components can call useQuery with the same key. They read the same cache entry, so only one request goes out, and both update together when it lands.
?. reads a field only if the value exists; ?? gives a fallback for undefined.
QuizInteractive · disabled in the PDF
Catalogue and BookCount appear on the same page at once. How many GET /books requests are sent?
Catalogue and BookCount appear on the same page at once. How many GET /books requests are sent?
TanStack Query44 / 183
Module 5 · Setting up and your first query
The devtools show you the cache
The cache is invisible, so TanStack Query ships a panel that shows it: every query, its key, its data and its state. It is only included in development builds, so it costs nothing in production.
No QueryClient set, use QueryClientProvider to set one
A useQuery is outside the provider
Query data cannot be undefined
Your query function returned nothing
data is possibly undefined (TypeScript)
You used data before checking isPending and isError
Find the bugInteractive · disabled in the PDF
This query logs "Query data cannot be undefined". Which line is the problem?
TSX
TanStack Query46 / 183
Module 5 · Setting up and your first query
Recap
Make one QueryClient, outside components, and provide it with QueryClientProvider.
useQuery({ queryKey, queryFn }) returns isPending, isError and data.
The key names the cache entry; the function only says how to fetch.
Components using the same key share one entry and one request.
The devtools show the cache while you develop.
FlashcardsInteractive · disabled in the PDF
What does a QueryClient do?
Owns the cache and decides when queries fetch.
What does QueryClientProvider do?
Puts the client in React context so any component below can use queries.
What is a query key?
An array naming a cache entry, such as ['books'].
What is a query function?
A function that returns a promise of the data, or throws when it fails.
What does isPending mean?
There is no data yet.
What is a context?
A value a React ancestor provides to every component below it.
Module 6: Query keys
TanStack Query47 / 183
Module 6 · Query keys
A key is the label on a cache entry
The cache is a map from keys to queries. Two calls with equal keys get the same entry; different keys get different entries. A key is an array, and it can hold strings, numbers and plain objects.
Key
Holds
['books']
The whole catalogue
['books', 42]
Book 42
['books', 42, 'reviews']
The reviews of book 42
['books', { search: 'dune' }]
Search results for "dune"
['reading-list']
Your reading list
Equal, not identical. A new array is created on every render, and that is fine. TanStack Query compares keys by their contents, not by which array object they are.
TanStack Query48 / 183
Module 6 · Query keys
Every input to the query belongs in the key
If the query function uses a value, such as a search term or a book id, that value must be in the key. Otherwise different answers share one label, and the cache can’t tell them apart.
Find the bugInteractive · disabled in the PDF
Open book 1, then book 2: the page still shows book 1. Which line is wrong?
TSX
TanStack Query49 / 183
Module 6 · Query keys
When the key changes, the query changes
A key is like an effect’s dependency array, but you never list dependencies separately. When the search text changes, the key changes, and the component simply reads a different entry: from the cache if it is there, from the server if not.
You search "dune", then "emma", then "dune" again. How many requests for "dune" are made?
You search "dune", then "emma", then "dune" again. How many requests for "dune" are made?
TanStack Query50 / 183
Module 6 · Query keys
Keys are compared by value
Internally a key is turned into a string, its hash, and entries are looked up by that string. Objects in a key are hashed with their fields sorted, so their order doesn’t matter. The order of the array’s items does.
QuizInteractive · disabled in the PDF
Which keys point at the same entry as ['books', { search: 'dune', page: 2 }]?
Which keys point at the same entry as ['books', { search: 'dune', page: 2 }]?
TanStack Query51 / 183
Module 6 · Query keys
Order keys from general to specific
Start every key with the kind of thing it holds, then narrow it down. Later you will act on groups of queries by a key’s beginning (its prefix): "everything about books", "every book list". A consistent shape makes those groups easy to name.
['books']
['books', 'list', …]
{ search: '' }
{ search: 'dune' }
['books', 'detail', 42]
['books', 'detail', 42, 'reviews']
One root per kind of data; lists and details as branches.
Pick one shape and keep it. Mixing ['book', 42] here and ['books', 42] there creates two entries for the same book, and invalidating one forgets the other.
TanStack Query52 / 183
Module 6 · Query keys
The query function is given its key
TanStack Query calls your query function with one argument, the query function context. It holds the queryKey it is fetching for, and a signal (more on that next). Reading values from the key keeps the key and the request from drifting apart.
[, , bookId] skips the first two items and names the third.
TanStack Query53 / 183
Module 6 · Query keys
The signal cancels requests nobody wants
An AbortSignal is a browser object that can tell a fetch to stop. Pass the context’s signal to fetch, and when the answer stops being wanted (the key changed, the component went away) TanStack Query aborts the request instead of letting it run to the end.
Fill the blanksInteractive · disabled in the PDF
Let TanStack Query cancel searches you have typed past.
The race is already gone. Each answer is stored under the key it was asked for. A slow "du" answer lands in the "du" entry, never in the "dune" one, so it can’t overwrite what you are looking at, signal or not.
TanStack Query54 / 183
Module 6 · Query keys
Write keys in one place
Typing keys by hand in every component invites typos. Keep them in one object of small functions, a key factory, and use it everywhere. Module 19 grows this into a full pattern.
as const keeps the exact types, so TypeScript knows the third item of a detail key is a number.
QuizInteractive · disabled in the PDF
Which keys start with bookKeys.detail(42)?
Which keys start with bookKeys.detail(42)?
TanStack Query55 / 183
Module 6 · Query keys
Try it: a key factory
Keys are plain arrays, so a key factory is plain code you can test without TanStack Query. The tests use startsWith, a simple version of the prefix check TanStack Query runs when you act on a group of queries (the real one also compares objects field by field, in any order).
Code challengeInteractive · disabled in the PDF
Finish bookKeys. lists() is ['books', 'list'], the parent of every list. list(search) adds { search } to it. detail(id) is ['books', 'detail', id], and reviews(id) adds 'reviews' to that.
TypeScript
1const bookKeys ={2all:['books']asconst,3// TODO: give each key its full shape4lists:()=>['books']asconst,5list:(search:string)=>['books']asconst,6detail:(id:number)=>['books']asconst,7reviews:(id:number)=>['books']asconst8}
TanStack Query56 / 183
Module 6 · Query keys
Recap
Equal keys share an entry; keys are compared by value, objects with sorted fields.
Every value the query function uses goes in the key.
Order keys general to specific so prefixes name useful groups.
The query function receives { queryKey, signal }; pass signal to fetch to cancel unwanted requests.
Keep keys in one key factory.
FlashcardsInteractive · disabled in the PDF
When do two queries share a cache entry?
When their keys are equal by value.
What must go in a query key?
Every value the query function depends on.
What is a key prefix?
The start of a key, used to match a group of queries.
What is the query function context?
The argument the query function receives: queryKey, signal and more.
What is an AbortSignal?
A browser object that can tell fetch to stop.
What is a key factory?
One object of functions that build every key, so keys stay consistent.
Module 7: Loading, errors and retries
TanStack Query57 / 183
Module 7 · Loading, errors and retries
status says whether there is data
A query result’s status is about the data: do we have any? It is one of three values, each with a matching shortcut.
status
Shortcut
Means
'pending'
isPending
No data yet
'error'
isError
The last fetch failed, and error holds why
'success'
isSuccess
data is there
Changed in version 5. Before version 5, "no data yet" was called loading and isLoading. In v5 it is pending and isPending, and isLoading means something narrower (two slides on).
TanStack Query58 / 183
Module 7 · Loading, errors and retries
fetchStatus says whether a request is running
The second question is separate: is the query function running right now? That is fetchStatus. A query can have data and be fetching (a background refresh), or have no data and not be fetching (it is waiting for something).
fetchStatus
Shortcut
Means
'fetching'
isFetching
The query function is running
'paused'
isPaused
It wanted to fetch, but the device is offline
'idle'
Nothing is running
QuizInteractive · disabled in the PDF
The catalogue is on screen and you switch back to its tab, which refetches it. What are status and fetchStatus during that refetch?
The catalogue is on screen and you switch back to its tab, which refetches it. What are status and fetchStatus during that refetch?
TanStack Query59 / 183
Module 7 · Loading, errors and retries
isLoading is the very first load
isLoading is isPending && isFetching: no data yet and a request running. It is the right flag for a first-load spinner. isPending alone is also true for a query that is waiting to start (module 9 shows queries you switch off).
Situation
isPending
isFetching
isLoading
First request running
true
true
true
Query switched off, never ran
true
false
false
Data shown, refetching
false
true
false
Data shown, nothing running
false
false
false
QuizInteractive · disabled in the PDF
You want a small "Updating…" note while the list refreshes in the background, with the list still visible. Which condition fits?
You want a small "Updating…" note while the list refreshes in the background, with the list still visible. Which condition fits?
TanStack Query60 / 183
Module 7 · Loading, errors and retries
Show every state, in order
TSX
1functionCatalogue(){2const books =useQuery({3queryKey:['books'],4queryFn:()=>getJson<Book[]>('/books')5})67if(books.isPending)return<Spinner/>8if(books.isError){9return<p>Could not load books: {books.error.message}</p>10}11return(12<>13{books.isFetching &&<small>Updating…</small>}14<BookListbooks={books.data}/>15</>16)17}
<>…</> is a fragment: two elements side by side without a wrapper.
In version 5, error is typed as Error by default, so error.message is safe. A good rule: check isPending, then isError, then use data.
TanStack Query61 / 183
Module 7 · Loading, errors and retries
A query only fails if its function throws
TanStack Query knows nothing about HTTP. If your function returns, the query succeeded, whatever it returned. Since fetch doesn’t reject on a 404 or 500 (module 2), the response.ok check is what turns them into query errors.
Find the bugInteractive · disabled in the PDF
The server is down and answers 500, but the query shows isSuccess with the error’s body as data. Which line is wrong?
TSX
TanStack Query62 / 183
Module 7 · Loading, errors and retries
Try it: a query function that fails properly
A query function is plain code: TanStack Query calls it with a context ({ queryKey, signal, … }) and reads the promise it returns. So you can test it alone, with fetch replaced by a mock, as here.
Code challengeInteractive · disabled in the PDF
Finish getReadingList. When the response isn’t ok, throw an Error whose message includes the status code, so the query fails instead of succeeding with an error body. Keep passing signal to fetch.
TypeScript
1typeEntry={ bookId:number; read:boolean}2const API ='https://api.example.com'34asyncfunctiongetReadingList({ signal }:{ signal:AbortSignal}){5const response =awaitfetch(`${API}/reading-list`,{ signal })6// TODO: a failed answer must fail the query7return(await response.json())asEntry[]8}
TanStack Query63 / 183
Module 7 · Loading, errors and retries
Failed queries are retried, three times by default
Networks fail for a moment all the time. So when a query function throws in the browser, TanStack Query waits and tries again, up to 3 more times, waiting longer each time (about 1, 2, then 4 seconds, never more than 30). This growing wait is called exponential backoff. Only after the last try does the query become error.
While retrying, status stays pending (or success, if it had data); failureCount counts the failed tries and failureReason holds the latest error.
If a background refetch fails, status becomes error, but data still holds the last good answer. Throwing away a perfectly good list because a refresh failed would be worse than showing it.
QuizInteractive · disabled in the PDF
The list showed fine; a refetch on tab focus fails after its retries. With the component from "Show every state, in order", what does the person see?
The list showed fine; a refetch on tab focus fails after its retries. With the component from "Show every state, in order", what does the person see?
Two more flags.isLoadingError is an error on the very first fetch (no data); isRefetchError is an error on a refetch (data kept). And refetch() from the result tries again on demand, for a "Try again" button.
TanStack Query66 / 183
Module 7 · Loading, errors and retries
Recap
status (pending, error, success) is about data; fetchStatus (fetching, paused, idle) is about requests.
isLoading is isPending && isFetching: the first load only.
A query fails only if its function throws, so check response.ok.
Failures retry 3 times with exponential backoff; skip retries for 4xx with a retry function.
A failed refetch keeps the previous data.
FlashcardsInteractive · disabled in the PDF
status vs fetchStatus?
status: do we have data? fetchStatus: is a request running now?
What is isLoading in v5?
isPending && isFetching: no data and the first request is running.
How many times does a failing query retry by default?
3 more times in the browser, with growing waits.
What is exponential backoff?
Waiting longer before each retry: about 1, 2, 4 seconds and so on.
What is failureCount?
How many attempts have failed so far for the current fetch.
What happens to data when a refetch fails?
It keeps the last successful value.
Module 8: Freshness, refetching and garbage collection
TanStack Query67 / 183
Module 8 · Freshness, refetching and garbage collection
Every cached answer is fresh, then stale
When an answer arrives it is fresh: TanStack Query trusts it and won’t fetch it again. After staleTime milliseconds it becomes stale: still shown, but worth checking. The default staleTime is 0, so data is stale the moment it arrives.
Why the default is 0. Showing out-of-date data by mistake is worse than an extra request, so TanStack Query errs towards checking. You raise staleTime for data you know changes slowly.
TanStack Query68 / 183
Module 8 · Freshness, refetching and garbage collection
Stale queries refetch on certain events
Stale data isn’t refetched constantly. It is refetched when something suggests the person is looking again. Each trigger can be turned off per query.
Trigger
Option (default)
A component using the query mounts (appears)
refetchOnMount: true
The browser tab or window regains focus
refetchOnWindowFocus: true
The device reconnects to the network
refetchOnReconnect: true
A timer, if you set one (it refetches fresh data too)
refetchInterval: false
Mount means a component appearing on screen; unmount means it going away. The refetch is in the background: the stale data stays on screen until the new answer replaces it.
QuizInteractive · disabled in the PDF
With the defaults, you open the catalogue, switch to your email for a minute, and come back. What happens?
With the defaults, you open the catalogue, switch to your email for a minute, and come back. What happens?
TanStack Query69 / 183
Module 8 · Freshness, refetching and garbage collection
Raise staleTime for data that changes slowly
Book details hardly ever change. Setting staleTime means "trust this for that long": while fresh, mounts and focus don’t refetch, and components share the data with no requests.
TSX
1useQuery({2queryKey:['books','detail', id],3queryFn:()=>getBook(id),4staleTime:5*60*1000// five minutes5})
Times are in milliseconds. Infinity means it never goes stale on its own.
Data
A reasonable staleTime
Book details, author bios
Minutes
Search results, the catalogue
30 seconds to a minute
Your reading list, which you edit
0, and refetch after edits
A list of genres that only changes with a release
Infinity
TanStack Query70 / 183
Module 8 · Freshness, refetching and garbage collection
Predict the requests
Predict the outputInteractive · disabled in the PDF
Every component uses the key ['genres'] with staleTime: 60_000 (one minute). How many requests are made?
Text
10:00 GenreMenu mounts20:20 GenreFilter mounts30:50 you switch tabs and come back41:30 you switch tabs and come back
Every component uses the key ['genres'] with staleTime: 60_000 (one minute). How many requests are made?
TanStack Query71 / 183
Module 8 · Freshness, refetching and garbage collection
Try it: the staleness rule
Inside TanStack Query, freshness is a small calculation. A query with no data yet is stale. Otherwise it stays fresh until staleTime milliseconds have passed since dataUpdatedAt, the moment its data arrived. Write that rule yourself.
Code challengeInteractive · disabled in the PDF
Write isStale(state, staleTime, now). Return true when state.data is undefined, or when at least staleTime milliseconds have passed since state.dataUpdatedAt; otherwise false.
TypeScript
1typeQueryState={ data:unknown; dataUpdatedAt:number}23functionisStale(4 state:QueryState, staleTime:number, now:number5):boolean{6// TODO: no data, or older than staleTime7returnfalse8}
TanStack Query72 / 183
Module 8 · Freshness, refetching and garbage collection
Unused data waits five minutes, then goes
When the last component using a query unmounts, the query becomes inactive. It stays in the cache in case you come back, for gcTime (garbage collection time, 5 minutes by default). Then it is deleted. Garbage collection means freeing memory nobody uses.
At least one component is using the query.
The last one unmounted. The data stays, so going back shows it instantly.
Nobody came back within gcTime, so the entry is deleted.
Renamed in version 5.gcTime was called cacheTime before v5. That name suggested how long data is cached, but the timer only runs once a query is unused, which is why it was renamed.
TanStack Query73 / 183
Module 8 · Freshness, refetching and garbage collection
staleTime and gcTime answer different questions
staleTime
Should I refetch this?
Counts from when the data arrived
Checked whenever a fetch might happen
Default: 0
gcTime
Should I keep this at all?
Counts from when the last user unmounted
Applies only while inactive
Default: 5 minutes
QuizInteractive · disabled in the PDF
Defaults everywhere. You leave the catalogue for a book page and return after 2 minutes. What happens?
Defaults everywhere. You leave the catalogue for a book page and return after 2 minutes. What happens?
TanStack Query74 / 183
Module 8 · Freshness, refetching and garbage collection
Set defaults on the client, override per query
Options on useQuery apply to that call. Defaults for every query go on the client. A query’s own options win over the defaults.
Don’t switch focus refetching off to hide requests. If requests on focus feel like too many, raise staleTime: fresh data isn’t refetched. Turning refetchOnWindowFocus off everywhere means people see stale data after coming back from lunch.
TanStack Query75 / 183
Module 8 · Freshness, refetching and garbage collection
Polling, and refetching by hand
Polling means fetching on a timer. For data that changes on its own, such as how many copies of a book are left, refetchInterval refetches every so many milliseconds, fresh or stale, but only while the tab is visible unless you also set refetchIntervalInBackground: true.
Module 8 · Freshness, refetching and garbage collection
Reading freshness in the devtools
The devtools mark each query with its state: fresh, fetching, paused, stale or inactive, plus how many components observe it (use it). Watching one query move through those states makes this module stick.
Try itInteractive · disabled in the PDF
TanStack Query77 / 183
Module 8 · Freshness, refetching and garbage collection
Recap
Data is fresh for staleTime (default 0), then stale.
Stale queries refetch in the background on mount, window focus and reconnect; refetchInterval refetches on a timer, fresh or not.
Unused queries are inactive, kept for gcTime (default 5 minutes), then removed.
Raise staleTime to cut requests; set defaults on the client and override per query.
FlashcardsInteractive · disabled in the PDF
What is staleTime?
How long new data counts as fresh and won’t be refetched. Default 0.
What is gcTime?
How long an unused query stays cached before it is removed. Default 5 minutes.
What makes a stale query refetch?
A component mounting, window focus, reconnecting, or an interval.
What is an inactive query?
One no component is using right now.
What is polling?
Fetching on a timer, with refetchInterval.
What was gcTime called before v5?
cacheTime.
Module 9: Parallel, dependent and shaped queries
TanStack Query78 / 183
Module 9 · Parallel, dependent and shaped queries
Queries in one component run in parallel
Call useQuery twice and both requests start in the same render. They run in parallel (at the same time), so the page waits for the slower one, not for the two added up.
TSX
1functionBookPage({ id }:{ id:number}){2const book =useQuery({3queryKey:['books','detail', id],4queryFn:()=>getBook(id)5})6const reviews =useQuery({7queryKey:['books','detail', id,'reviews'],8queryFn:()=>getReviews(id)9})10// book and reviews load at the same time11}
QuizInteractive · disabled in the PDF
The book takes 300 ms and the reviews 500 ms. About how long until both are loaded?
The book takes 300 ms and the reviews 500 ms. About how long until both are loaded?
TanStack Query79 / 183
Module 9 · Parallel, dependent and shaped queries
useQueries for a list of queries
Hooks can’t be called in a loop, because React needs the same hooks in the same order on every render. When the number of queries depends on data, such as one query per book on your reading list, useQueries takes an array of query options.
combine (new in v5) turns the array of results into one value.
Shared entries. Each item uses the same key as BookPage, so any book you already opened shows from the cache, and opening one afterwards is instant.
TanStack Query80 / 183
Module 9 · Parallel, dependent and shaped queries
enabled: false holds a query back
Sometimes a query needs something you don’t have yet. enabled switches a query on or off: while it is false, the query doesn’t fetch, its status is pending and its fetchStatus is idle. A query that waits for another is a dependent query.
The ! tells TypeScript "this is defined here"; enabled is what makes that true.
QuizInteractive · disabled in the PDF
While the book is still loading, what are the author query’s isPending and isLoading?
While the book is still loading, what are the author query’s isPending and isLoading?
TanStack Query81 / 183
Module 9 · Parallel, dependent and shaped queries
skipToken: switch off, with better types
Instead of enabled plus a !, you can pass skipToken as the query function when an input is missing. TypeScript then knows the id is defined in the other branch, without forcing it.
No refetch while skipped. Calling refetch() on a query whose function is skipToken can’t work: there is no function to call. Use enabled if you need a manual refetch.
TanStack Query82 / 183
Module 9 · Parallel, dependent and shaped queries
select reshapes the data for one component
select is a function from the cached data to what this component wants. The cache keeps the full answer; each component can pick its own view. The component only renders again when its selected value changes.
A refetch returns the reading list with one book’s note edited, but the same number of books. Does ReadingListCount render again?
A refetch returns the reading list with one book’s note edited, but the same number of books. Does ReadingListCount render again?
TanStack Query83 / 183
Module 9 · Parallel, dependent and shaped queries
Keep the previous results while new ones load
Change the search and the key changes, so the new entry has no data: the list vanishes behind a spinner on every keystroke. placeholderData: keepPreviousData keeps showing the last results until the new ones arrive, and sets isPlaceholderData so you can dim them.
Changed in version 5. v4’s keepPreviousData: true option became placeholderData: keepPreviousData in v5. Placeholder data is shown but never written to the cache.
TanStack Query84 / 183
Module 9 · Parallel, dependent and shaped queries
initialData seeds the cache from what you already have
The search results already hold each book’s title and author. When you open a book, you can start its details query with that copy. initialData is put in the cache as real data, so treat it like a real answer: initialDataUpdatedAt says how old it is, so freshness still works.
initialData
Stored in the cache as real data
Shared with every user of the key
Use when the copy is complete and correct
placeholderData
Shown, never stored
Only for this observer
Use for a preview or the previous results
QuizInteractive · disabled in the PDF
Search results have a book’s title only, but the details page also shows its description. Which should you use while the details load?
Search results have a book’s title only, but the details page also shows its description. Which should you use while the details load?
TanStack Query85 / 183
Module 9 · Parallel, dependent and shaped queries
Waterfalls: requests that wait for no reason
A waterfall is requests running one after another when they could have run together. Dependent queries are a necessary waterfall. An accidental one comes from a child that only mounts, and so only fetches, once its parent’s query has finished.
Find the bugInteractive · disabled in the PDF
The reviews don’t start loading until the book has loaded, though they only need the id. Which line causes it?
TSX
TanStack Query86 / 183
Module 9 · Parallel, dependent and shaped queries
Recap
Several useQuery calls run in parallel; useQueries handles a dynamic list, and combine merges results.
enabled: false or skipToken holds a query back; that is how dependent queries wait.
select gives each component its own view and fewer renders.
placeholderData shows something without caching it; initialData is cached as real data.
Watch for accidental waterfalls caused by children mounting late.
FlashcardsInteractive · disabled in the PDF
What is a dependent query?
A query that waits (with enabled or skipToken) for data from another.
What does combine do in useQueries?
Turns the array of results into a single value of your choice.
What does select do?
Transforms cached data for one component, which then only renders when that result changes.
What does keepPreviousData do?
Shows the previous key’s data as a placeholder while the new key loads.
initialData vs placeholderData?
initialData is stored in the cache; placeholderData is only shown.
What is a waterfall?
Requests made one after another when they could have run together.
Module 10: Mutations: writing to the server
TanStack Query87 / 183
Module 10 · Mutations: writing to the server
Queries read; mutations write
A mutation is a request that changes something on the server: posting a review, adding a book to your reading list. It differs from a query in three ways.
Lines 2:useMutation returns an object to start the write and follow its progress.
Lines 3-4: The mutation function: takes the variables you pass and returns a promise. postReview is the fetch with POST from module 2.
Lines 10: Stop the browser’s default form submit, which would reload the page.
Lines 11:mutate runs the mutation with these variables. Nothing happens until it is called.
Mutation function
Your function that performs the write and returns a promise.
Variables
The single value passed to mutate, handed to the mutation function.
TanStack Query89 / 183
Module 10 · Mutations: writing to the server
A mutation has its own status
status
Shortcut
Means
'idle'
isIdle
Not started (or reset() was called)
'pending'
isPending
The request is running
'error'
isError
It failed; error holds why
'success'
isSuccess
It worked; data holds the server’s answer
Fill the blanksInteractive · disabled in the PDF
Disable the button and change its label while the review is being posted.
TSX
1<buttondisabled={post.}>2{post.isPending ?'Posting…':'Post review'}3</button>4{post.&&<p>Could not post: {post.error.message}</p>}
TanStack Query90 / 183
Module 10 · Mutations: writing to the server
After a write, the cache is out of date
The review was saved, but the reviews query still holds the list from before. Nothing tells the cache that the server changed: the mutation isn’t linked to any key. You have to say which queries it affected.
Invalidating a query marks it stale and refetches it now if it is on screen. You do it through the client, which any component gets from useQueryClient().
TSX
1const queryClient =useQueryClient()23const post =useMutation({4mutationFn:(review:NewReview)=>postReview(bookId, review),5onSuccess:()=>{6return queryClient.invalidateQueries({7queryKey:['books','detail', bookId,'reviews']8})9}10})
TanStack Query91 / 183
Module 10 · Mutations: writing to the server
Return the invalidation to stay pending until it is done
invalidateQueries returns a promise that settles when the refetch has finished. Returning it from onSuccess keeps the mutation pending until then, so the button says "Posting…" until the new review is actually on screen.
QuizInteractive · disabled in the PDF
You call invalidateQueries in onSuccess without returning it. The form clears when isPending turns false. What might the person see?
You call invalidateQueries in onSuccess without returning it. The form clears when isPending turns false. What might the person see?
TanStack Query92 / 183
Module 10 · Mutations: writing to the server
Callbacks: on the hook and on each call
useMutation takes onMutate (before the request), onSuccess, onError and onSettled (after either). mutate takes onSuccess, onError and onSettled too, for this one call. Put cache work on the hook, and screen work (closing a dialog, a toast) on the call.
A mutation succeeds. Put its callbacks in the order they run.
1.onSettled on the hook
2.onMutate on the hook
3.The mutation function runs
4.onSuccess on the mutate call
5.onSuccess on the hook
TanStack Query93 / 183
Module 10 · Mutations: writing to the server
mutateAsync when you need to wait
mutate returns nothing and never throws: errors go to onError and error. mutateAsync returns a promise, for when the next step must wait for the write, and you must catch its errors yourself.
try{const saved =await post.mutateAsync(review)goTo(saved.id)}catch{// post.error has it too}
Find the bugInteractive · disabled in the PDF
When the server rejects the review, the console shows "Uncaught (in promise)". Which line is the cause?
TSX
TanStack Query94 / 183
Module 10 · Mutations: writing to the server
Show the server’s reasons
When the server answers 400 with a reason ("A review needs at least 10 characters"), put that reason in the error you throw, and show it next to the form.
.catch(() => ({})) copes with an error answer that isn’t JSON.
TanStack Query95 / 183
Module 10 · Mutations: writing to the server
Recap
Mutations write; they run only when you call mutate, aren’t cached, and aren’t retried.
isPending drives the button; error holds what went wrong.
Invalidate the affected queries in onSuccess, and return the promise to stay pending until they refetch.
Cache work goes in the hook’s callbacks; screen work in the call’s.
Prefer mutate; use mutateAsync only when you must wait, and catch its errors.
FlashcardsInteractive · disabled in the PDF
What is a mutation?
A request that changes data on the server.
What are a mutation’s variables?
The value passed to mutate, handed to the mutation function.
What does invalidateQueries do?
Marks matching queries stale and refetches those on screen.
What does useQueryClient return?
The QueryClient from the nearest provider.
mutate vs mutateAsync?
mutate never throws and returns nothing; mutateAsync returns a promise that can reject.
Which runs first: the hook’s onSuccess or the call’s?
The hook’s.
Module 11: Invalidation and writing to the cache
TanStack Query96 / 183
Module 11 · Invalidation and writing to the cache
Invalidating marks stale, then refetches what is on screen
invalidateQueries does two things to every matching query: it marks it stale (whatever its staleTime), and it refetches it right away if it is active. Inactive matches just stay stale, and refetch the next time something mounts them.
QuizInteractive · disabled in the PDF
You add a book to your reading list from its details page. The reading list page isn’t open, but its query is cached. You invalidate ['reading-list']. When is it refetched?
You add a book to your reading list from its details page. The reading list page isn’t open, but its query is cached. You invalidate ['reading-list']. When is it refetched?
TanStack Query97 / 183
Module 11 · Invalidation and writing to the cache
Filters choose which queries to touch
Most client methods take query filters. By default queryKey matches by prefix: every query whose key starts with it. Other filters narrow it down.
Filter
Matches
queryKey: ['books']
Every key starting with books
exact: true
Only that exact key
type: 'active'
Only queries on screen (also inactive, all)
stale: true
Only stale queries
predicate: (query) => …
Whatever your function says, given each query
TypeScript
1// Every book list, but not book details2queryClient.invalidateQueries({queryKey:['books','list']})34// Just the catalogue with no search5queryClient.invalidateQueries({6queryKey:['books','list',{search:''}],7exact:true8})
Controlling the refetch.invalidateQueries also takes refetchType: 'active' (the default), 'inactive', 'all', or 'none' to mark stale without refetching anything.
TanStack Query98 / 183
Module 11 · Invalidation and writing to the cache
Ask: what did this write change?
For each mutation, list everything on the server it changed, then the queries that show it. Adding a book to the reading list changes the list itself, and the "on your list" badge in the book’s details.
QuizInteractive · disabled in the PDF
Posting a review changes the reviews and the book’s average rating, shown in its details and in every list. Which invalidation covers it with the fewest refetches?
Posting a review changes the reviews and the book’s average rating, shown in its details and in every list. Which invalidation covers it with the fewest refetches?
Over-invalidating is the safe mistake. Refetching too much costs a few requests; refetching too little shows wrong data. When in doubt, invalidate the wider prefix. Only active queries refetch anyway.
TanStack Query99 / 183
Module 11 · Invalidation and writing to the cache
When the server sends the new data back, store it
Many APIs answer a write with the updated thing. Then you don’t need a refetch: write the answer straight into the cache with setQueryData. Everyone reading that key updates at once.
onSuccess receives the mutation function’s result first.
setQueryData also takes an updater function, given the current data (or undefined if there is none), that returns the new data. Use it to change part of a list.
TanStack Query100 / 183
Module 11 · Invalidation and writing to the cache
Never change cached data in place
Cached data must be treated as immutable: never modified, only replaced by a new copy. TanStack Query and React notice changes by comparing old and new values; if you edit the old object, there is nothing to compare, and nothing re-renders.
Find the bugInteractive · disabled in the PDF
The new review is in the cache, but the list on screen never shows it. Which line is wrong?
TypeScript
TanStack Query101 / 183
Module 11 · Invalidation and writing to the cache
Write or refetch?
setQueryData
No extra request
Instant
You must get the shape exactly right
Only updates the keys you write
invalidateQueries
One request per active match
A short wait
The server computes the truth (sorting, counts, ratings)
Catches everything under a prefix
Often you do both: write the detail you got back, and invalidate the lists whose order or counts the server computes.
QuizInteractive · disabled in the PDF
You add a book to the reading list. The server answers 204 No Content, and the list is sorted on the server by date added. What should onSuccess do?
You add a book to the reading list. The server answers 204 No Content, and the list is sorted on the server by date added. What should onSuccess do?
TanStack Query102 / 183
Module 11 · Invalidation and writing to the cache
Reading the cache outside a component
getQueryData(key) returns what is cached under an exact key, or undefined, without fetching. fetchQuery fetches if the data is missing or stale and returns a promise of it; ensureQueryData returns cached data if there is any, and only fetches if not.
Changed in v5.102. Since v5.102, one method, queryClient.query(options), replaces the other three: it works like fetchQuery; add .catch(() => {}) for a prefetch, or staleTime: 'static' (data that never goes stale) for ensureQueryData. The three still work in every v5 release, but are deprecated and will go in v6. This course uses them, since they run on any v5.
TanStack Query103 / 183
Module 11 · Invalidation and writing to the cache
Invalidate after every mutation, in one place
Instead of writing onSuccess in every mutation, the client’s MutationCache can react to all of them. Each mutation can carry a meta object: free-form information you attach for code like this.
A name starting with _ signals an argument you don’t use. The : QueryClient is needed because the client’s own options refer to it.
Even simpler. Some apps simply invalidate everything after any mutation (queryClient.invalidateQueries() with no filter). Only active queries refetch, so on a small app it is cheap and never wrong. Narrow it when the requests start to matter.
TanStack Query104 / 183
Module 11 · Invalidation and writing to the cache
Recap
Invalidation marks matches stale and refetches the active ones; inactive ones refetch when next used.
Filters match by key prefix by default; narrow with exact, type, stale or predicate.
setQueryData writes an answer straight into the cache; always return new objects.
Write what the server sent back; invalidate what the server computes.
MutationCache callbacks and meta centralise invalidation.
FlashcardsInteractive · disabled in the PDF
What are query filters?
Options like queryKey, exact, type and predicate that choose which queries a method acts on.
How does queryKey match in a filter by default?
By prefix: every key that starts with it.
What does immutable mean?
Never changed in place; replaced with a new copy instead.
What does setQueryData do?
Writes data into the cache under a key, notifying everyone reading it.
fetchQuery vs prefetchQuery?
Both fetch if needed; fetchQuery returns the data and can reject, prefetchQuery returns nothing and never rejects.
What is meta?
Free-form information attached to a query or mutation, for your own global code.
Module 12: Optimistic updates
TanStack Query105 / 183
Module 12 · Optimistic updates
Optimistic updates show success before the server says so
Most writes succeed, and waiting for the round trip makes an app feel slow. An optimistic update changes the screen as if the write had already worked, then reconciles with the server: keep it if it worked, undo it if not.
Good candidates
Better to wait
Ticking "read" on a reading list book
Paying for an order
Removing a book from your list
Deleting your account
Liking a review
Writes the server often rejects (validation)
QuizInteractive · disabled in the PDF
Which is the best candidate for an optimistic update?
Which is the best candidate for an optimistic update?
TanStack Query106 / 183
Module 12 · Optimistic updates
The simple way: show the pending variables
While a mutation runs, variables holds what you sent. Render it as an extra, dimmed item; when the mutation settles, invalidate so the real list replaces it. Nothing in the cache changes, so there is nothing to undo: if it fails, the dimmed item becomes a failed one with a retry button.
From another component. If the list lives in a different component, give the mutation a mutationKey and read the pending variables there with useMutationState({ filters: { mutationKey, status: 'pending' }, select: (m) => m.state.variables }).
TanStack Query107 / 183
Module 12 · Optimistic updates
The cache way: write, remember, roll back
When the change must show everywhere the data appears, write it into the cache in onMutate, keep a snapshot of what was there, and put the snapshot back in onError (a rollback). Whatever happens, invalidate in onSettled so the cache ends up matching the server.
Lines 3-4: Stop any refetch of the list that is already running, so an older answer can’t land on top of the optimistic one.
Lines 5: Take the snapshot: the list as it was.
Lines 6-8: Write the expected result: the list without that book. Every component showing it updates at once.
Lines 9: Whatever onMutate returns is handed to onSuccess, onError and onSettled as their third argument. Since v5.89 the types call it onMutateResult; older docs call it the context.
Lines 11-13: On failure, put the snapshot back.
Lines 14-15: Success or not, refetch, so the cache matches the server rather than your guess.
TanStack Query108 / 183
Module 12 · Optimistic updates
Why cancel queries first
QuizInteractive · disabled in the PDF
You skip cancelQueries. A refetch of the reading list started just before the click, and answers 100 ms after your optimistic write. What does the person see?
You skip cancelQueries. A refetch of the reading list started just before the click, and answers 100 ms after your optimistic write. What does the person see?
Put in orderInteractive · disabled in the PDF
Put a cache-based optimistic update in order.
1.Cancel running queries for the key
2.The mutation function runs against the server
3.Write the optimistic data into the cache
4.Invalidate the key so it matches the server
5.On error, restore the snapshot
6.Snapshot the current data
TanStack Query109 / 183
Module 12 · Optimistic updates
Take the snapshot before you write
Find the bugInteractive · disabled in the PDF
When the server fails, the book stays removed. Which line is in the wrong place?
TypeScript
TanStack Query110 / 183
Module 12 · Optimistic updates
Optimistic updates inside one item
Ticking a book as read changes one field of one item. Map over the list and replace only that item with a changed copy, keeping everything else as it was.
Fill the blanksInteractive · disabled in the PDF
Mark one entry as read in the cached list.
TypeScript
1queryClient.setQueryData<Entry[]>(listKey,(old)=>2 old?.((entry)=>3 entry.bookId === id ?{,read:true}: entry4)5)
TanStack Query111 / 183
Module 12 · Optimistic updates
Try it: the updater function
The function you pass to setQueryData is an updater: it gets the cached data (or undefined if there is none) and returns the new data. It is plain code, so it can be written and tested on its own, then dropped into onMutate.
Code challengeInteractive · disabled in the PDF
Write markRead(old, bookId). Return a new list where the entry with that bookId is a copy with read: true, and every other entry is the very same object as before. Never change old; if old is undefined, return undefined.
TypeScript
1typeEntry={ id:string; bookId:number; read:boolean}23functionmarkRead(old:Entry[]|undefined, bookId:number){4// TODO: a new list, with a changed copy of one entry5 old?.forEach((entry)=>{6if(entry.bookId === bookId) entry.read =true7})8return old9}
TanStack Query112 / 183
Module 12 · Optimistic updates
Overlapping mutations
Tick three books quickly. The first mutation settles and invalidates the list, and the refetch might answer before the other two writes reached the server, briefly un-ticking them. One fix: only invalidate when this is the last mutation of its kind still running.
TypeScript
1useMutation({2mutationKey:['reading-list','mark-read'],3mutationFn:(bookId:number)=>saveRead(bookId),4// onMutate and onError as before5onSettled:()=>{6const running = queryClient.isMutating({7mutationKey:['reading-list','mark-read']8})9if(running ===1){10return queryClient.invalidateQueries({queryKey: listKey })11}12}13})
isMutating counts running mutations; this one still counts while its onSettled runs.
Snapshots and overlap. With overlapping writes, a rollback restores a snapshot that may include another write’s optimistic change, or miss one. For writes that can overlap a lot, prefer the variables approach, or roll back only the item that failed instead of the whole list.
TanStack Query113 / 183
Module 12 · Optimistic updates
New items need a temporary id
When you add something optimistically, the server hasn’t given it an id yet, but lists need a key. Make a temporary one, mark the item as pending, and let the invalidation replace it with the real item.
TypeScript
1onMutate:async(bookId:number)=>{2await queryClient.cancelQueries({queryKey: listKey })3const before = queryClient.getQueryData<Entry[]>(listKey)4const temp:Entry={5id:`temp-${crypto.randomUUID()}`,6 bookId,7read:false,8pending:true9}10 queryClient.setQueryData<Entry[]>(listKey,(old)=>[11...(old ??[]),12 temp13])14return{ before }15}
crypto.randomUUID() is built into browsers and makes a random, unique string. Entry gets an optional pending?: boolean field for this.
QuizInteractive · disabled in the PDF
While the item is pending, the person clicks "Remove" on it. What is the problem?
While the item is pending, the person clicks "Remove" on it. What is the problem?
TanStack Query114 / 183
Module 12 · Optimistic updates
Recap
Optimistic updates suit writes that rarely fail and are easy to undo.
Simple way: render the mutation’s variables until it settles, then invalidate.
Cache way: in onMutate cancel, snapshot, write, return the snapshot; roll back in onError; invalidate in onSettled.
Always build new arrays and objects; give new items temporary ids.
With overlapping writes, invalidate only when the last one settles.
FlashcardsInteractive · disabled in the PDF
What is an optimistic update?
Showing a write’s result before the server confirms it, undoing it if it fails.
What is a rollback?
Putting the cache back as it was before a failed optimistic write.
Why call cancelQueries in onMutate?
So an older answer already on its way can’t overwrite the optimistic data.
What is onMutateResult in mutation callbacks?
Whatever onMutate returned, passed to onSuccess, onError and onSettled (older docs call it the context).
What does useMutationState give you?
The state of mutations matching a filter, readable from any component.
What does isMutating return?
How many mutations matching a filter are running.
Module 13: Pagination and infinite lists
TanStack Query115 / 183
Module 13 · Pagination and infinite lists
Big lists come in pages
A catalogue of 50,000 books can’t come back in one answer. APIs split lists into pages. There are two common ways to ask for the next one.
Offset (page numbers)
/books?page=3 or ?offset=40&limit=20
Jump to any page
Rows shift if books are added while you read
Cursor
/books?cursor=abc from the previous answer
Only "next" (and maybe "previous")
Stable while the list changes
Pagination
Splitting a long list into pages fetched one at a time.
Cursor
An opaque marker the server gives you, meaning "continue from here".
TanStack Query116 / 183
Module 13 · Pagination and infinite lists
Numbered pages: the page goes in the key
Each page is its own query, so the page number goes in the key. Add keepPreviousData (module 9) so the current page stays visible while the next one loads, instead of collapsing to a spinner.
TSX
1const[page, setPage]=useState(1)23const books =useQuery({4queryKey:['books','list',{ search, page }],5queryFn:()=>getBookPage(search, page),6placeholderData: keepPreviousData7})89<button10onClick={()=>setPage((p)=> p +1)}11disabled={books.isPlaceholderData ||!books.data?.hasMore}12>13 Next page14</button>
The answer looks like { books: Book[], hasMore: boolean }.
QuizInteractive · disabled in the PDF
Why disable "Next page" while isPlaceholderData is true?
Why disable "Next page" while isPlaceholderData is true?
TanStack Query117 / 183
Module 13 · Pagination and infinite lists
Fetch the next page before it is asked for
prefetchQuery fetches into the cache without any component using it. Prefetch page n + 1 as soon as page n arrives, and "Next" is instant.
Same key, same options. The prefetch must use exactly the key the page will use, or the page won’t find it. Module 19 shares the options so they can’t drift.
TanStack Query118 / 183
Module 13 · Pagination and infinite lists
useInfiniteQuery: many pages in one entry
For a "Load more" list, you want every loaded page together, under one key. useInfiniteQuery stores pages in a list and asks you how to get the next one.
Loading on scroll. For endless scrolling, put an empty element at the end of the list and call fetchNextPage when it scrolls into view. The browser’s IntersectionObserver tells you when an element becomes visible.
TanStack Query121 / 183
Module 13 · Pagination and infinite lists
Refetching an infinite list refetches every page
When an infinite query refetches (on focus, or after invalidation), it refetches all loaded pages, one after another starting from the first, so cursors stay consistent. Ten loaded pages means ten requests in a row.
QuizInteractive · disabled in the PDF
Five pages are loaded. You invalidate the feed. What happens?
Five pages are loaded. You invalidate the feed. What happens?
Limit the pages kept.maxPages (v5) keeps only the last N pages in the cache, dropping the oldest as new ones load. It bounds memory and refetch cost; to scroll back up you then need getPreviousPageParam and fetchPreviousPage.
TanStack Query122 / 183
Module 13 · Pagination and infinite lists
Choosing between them
Need
Use
"Page 3 of 40", jump to a page
useQuery with the page in the key + keepPreviousData
A feed with "Load more" or endless scroll
useInfiniteQuery
Instant "Next"
Either, plus prefetching the next page
QuizInteractive · disabled in the PDF
A search results page shows 20 books, with numbered page links at the bottom. Which fits?
A search results page shows 20 books, with numbered page links at the bottom. Which fits?
TanStack Query123 / 183
Module 13 · Pagination and infinite lists
Recap
Numbered pages: put the page in the key and keep the previous data while the next loads.
prefetchQuery the next page for instant navigation.
useInfiniteQuery keeps every page under one key: initialPageParam, getNextPageParam, data.pages.
fetchNextPage, hasNextPage and isFetchingNextPage drive "Load more".
Refetching an infinite query refetches every page in order; maxPages limits that.
FlashcardsInteractive · disabled in the PDF
What is a cursor?
A marker from the server meaning "continue from here".
What is pageParam?
The value an infinite query passes to the query function to say which page to fetch.
What must getNextPageParam return when there are no more pages?
undefined or null.
What does data.pages hold?
Every loaded page’s answer, in order.
What does prefetchQuery do?
Fetches into the cache with no component using it; never throws.
What does maxPages do?
Keeps only the last N pages of an infinite query.
Module 14: Prefetching, Suspense and error boundaries
TanStack Query124 / 183
Module 14 · Prefetching, Suspense and error boundaries
Fetch on hover, show on click
People hover over a link a few hundred milliseconds before they click it. Prefetch the book on hover, and by the time the details page mounts, its data is already in the cache.
TSX
1functionBookLink({ book }:{ book:Book}){2const queryClient =useQueryClient()3constprefetch=()=>4 queryClient.prefetchQuery({5queryKey:['books','detail', book.id],6queryFn:()=>getBook(book.id),7staleTime:60_0008})9return(10<ahref={`/books/${book.id}`}onMouseEnter={prefetch}>11{book.title}12</a>13)14}
QuizInteractive · disabled in the PDF
You hover the same link five times within ten seconds. How many requests does the prefetch send?
You hover the same link five times within ten seconds. How many requests does the prefetch send?
TanStack Query125 / 183
Module 14 · Prefetching, Suspense and error boundaries
Fetch when navigation starts
Many routers let you run code when a page is about to open: a loader. Starting the queries there, instead of in the page’s components, means they begin before React renders anything, with no waterfall. ensureQueryData returns cached data if there is some, or fetches it.
TypeScript
1// A route loader, in whichever router you use2asyncfunctionbookPageLoader(id:number){3awaitPromise.all([4 queryClient.ensureQueryData({5queryKey:['books','detail', id],6queryFn:()=>getBook(id)7}),8 queryClient.prefetchQuery({9queryKey:['books','detail', id,'reviews'],10queryFn:()=>getReviews(id)11})12])13}
Promise.all waits for several promises at once.
The components still use `useQuery`. Prefetching only fills the cache. The page still calls useQuery with the same keys; it just finds the data there.
TanStack Query126 / 183
Module 14 · Prefetching, Suspense and error boundaries
Suspense moves loading up to a boundary
Suspense is a React feature: a component can say "I am not ready" (it suspends), and the nearest <Suspense> above it shows its fallback instead. useSuspenseQuery suspends until the data exists, so data is never undefined and there is no isPending branch.
TSX
1functionBookDetails({ id }:{ id:number}){2const{ data }=useSuspenseQuery({3queryKey:['books','detail', id],4queryFn:()=>getBook(id)5})6return<h1>{data.title}</h1>7}89<Suspensefallback={<Spinner/>}>10<BookDetailsid={42}/>11</Suspense>
useSuspenseQuery has no enabled and no placeholderData: it always runs and always has real data.
There are also useSuspenseQueries and useSuspenseInfiniteQuery.
A failed background refetch keeps the data; only a failure with nothing to show goes to an error boundary.
TanStack Query127 / 183
Module 14 · Prefetching, Suspense and error boundaries
Suspense queries in one component run one after another
Predict the outputInteractive · disabled in the PDF
Each request takes 1 second. Roughly how long until BookPage shows?
TSX
1functionBookPage({ id }:{ id:number}){2const book =useSuspenseQuery(bookQuery(id))3const reviews =useSuspenseQuery(reviewsQuery(id))4return<Pagebook={book.data}reviews={reviews.data}/>5}
Each request takes 1 second. Roughly how long until BookPage shows?
Module 14 · Prefetching, Suspense and error boundaries
Error boundaries catch what a region throws
An error boundary is a component that catches errors thrown while rendering anything inside it, and shows a fallback instead of a blank page. React only offers it as a class component, so most apps use the small react-error-boundary package. With Suspense, failed queries are thrown to the nearest boundary.
TSX
1import{ ErrorBoundary }from'react-error-boundary'23<ErrorBoundaryfallback={<p>Could not load this book.</p>}>4<Suspensefallback={<Spinner/>}>5<BookDetailsid={id}/>6</Suspense>7</ErrorBoundary>
Without Suspense too. A plain useQuery can send its errors to a boundary with throwOnError: true, or a function such as (error) => !(error instanceof HttpError && error.status < 500) to throw only unexpected failures and handle a 404 in place.
TanStack Query129 / 183
Module 14 · Prefetching, Suspense and error boundaries
"Try again" needs a reset
When a boundary’s "Try again" renders its children again, a query still in its error state would throw again at once. QueryErrorResetBoundary gives you a reset that makes the queries inside fetch again on their next render instead. Pass it to the error boundary’s onReset.
Clicking "Try again" shows the error again instantly, with no request. Which line is missing something?
TSX
TanStack Query130 / 183
Module 14 · Prefetching, Suspense and error boundaries
Keep the old page while the new key suspends
With Suspense, changing the key (opening another book) suspends again and swaps the page for the fallback. Wrapping the state change in React’s startTransition tells React the change can wait: it keeps showing the current content until the new one is ready.
TSX
1const[isPending, startTransition]=useTransition()23<button4onClick={()=>startTransition(()=>setBookId(next))}5>6 Next book7</button>8{isPending &&<small>Loading…</small>}
QuizInteractive · disabled in the PDF
A component must show a book, and its data is guaranteed by a loader that already ran. Which hook gives the simplest component?
A component must show a book, and its data is guaranteed by a loader that already ran. Which hook gives the simplest component?
TanStack Query131 / 183
Module 14 · Prefetching, Suspense and error boundaries
Recap
Prefetch on hover or in a route loader so data is cached before the page needs it.
useSuspenseQuery suspends until data exists; loading moves to <Suspense>.
Several suspense queries in one component run in sequence; use useSuspenseQueries or prefetch.
Errors go to error boundaries; QueryErrorResetBoundary lets "Try again" refetch.
startTransition keeps the old content while a new key suspends.
FlashcardsInteractive · disabled in the PDF
What is Suspense?
A React feature: a component that isn’t ready suspends, and the nearest <Suspense> shows a fallback.
What is an error boundary?
A component that catches errors thrown inside it and shows a fallback.
What does throwOnError do?
Makes useQuery throw its error to the nearest error boundary.
What does QueryErrorResetBoundary provide?
A reset that lets failed queries refetch instead of throwing again.
What is a route loader?
Code a router runs before a page opens, a good place to prefetch.
What does ensureQueryData do?
Returns cached data if any, otherwise fetches and returns it.
Module 15: Testing queries and mutations
TanStack Query132 / 183
Module 15 · Testing queries and mutations
A test is code that checks your code
An automated test runs a piece of your app and checks that it did what you expect, so you find out when a change breaks it. Here: Vitest runs the tests, Testing Library renders components and finds things on screen the way a person would, and the network is replaced with a fake.
Test runner
A tool (here Vitest) that finds your tests, runs them and reports what failed.
Mock
A stand-in for something real, such as a fake fetch that answers instantly.
Assertion
A check inside a test, such as expect(x).toBe(3).
Flaky test
A test that sometimes passes and sometimes fails with no code change.
Goes with the testing course. The vitest course covers the runner itself. This module is about what TanStack Query adds: the client, the cache and asynchronous answers.
TanStack Query133 / 183
Module 15 · Testing queries and mutations
A fresh client for every test
A component under test needs a QueryClientProvider, like in the app. Make a new client in each test, so no cached data carries over from one to the next, and switch retries off, so a failing request fails at once instead of after 7 seconds of backoff.
Each test passes alone, but "shows an error" fails when run after "shows the books". Which line causes it?
TSX
TanStack Query134 / 183
Module 15 · Testing queries and mutations
Replace the network
Tests shouldn’t call a real server: it is slow, and its data changes. Either mock your API functions, or mock fetch itself. MSW (Mock Service Worker) is a popular library that answers requests at the network level, so your real fetch code runs too.
TypeScript
1import{ vi }from'vitest'2import*as api from'../api'34vi.spyOn(api,'getBooks').mockResolvedValue([5{id:1,title:'Emma',author:'Jane Austen'}6])
mockResolvedValue makes the function return a promise that fulfils with that value.
QuizInteractive · disabled in the PDF
What does mocking at the network level (with MSW) test that mocking getBooks doesn’t?
What does mocking at the network level (with MSW) test that mocking getBooks doesn’t?
TanStack Query135 / 183
Module 15 · Testing queries and mutations
Wait for what the person would wait for
Even a mocked answer arrives asynchronously, so the first render always shows the loading state. Testing Library’s findBy… queries wait (a second, by default) for an element to appear.
TSX
1test('shows the books',async()=>{2 vi.spyOn(api,'getBooks').mockResolvedValue(books)3renderWithClient(<Catalogue/>)45expect(screen.getByText('Loading…')).toBeInTheDocument()6expect(await screen.findByText('Emma')).toBeInTheDocument()7})
QuizInteractive · disabled in the PDF
Replacing await screen.findByText('Emma') with screen.getByText('Emma') makes the test fail. Why?
Replacing await screen.findByText('Emma') with screen.getByText('Emma') makes the test fail. Why?
TanStack Query136 / 183
Module 15 · Testing queries and mutations
Testing a custom hook
When a query lives in your own hook (such as useBook(id)), test it directly with renderHook, passing the provider as a wrapper. waitFor repeats a check until it passes.
Fill the blanksInteractive · disabled in the PDF
Render the hook inside a provider and wait for success.
TSX
1const client =newQueryClient({2defaultOptions:{queries:{retry:false}}3})4constwrapper=({ children }:{ children:React.ReactNode})=>(5<QueryClientProviderclient={client}>{children}</QueryClientProvider>6)78const{ result }=(()=>useBook(42),{})910await(()=>expect(result.current.isSuccess).toBe(true))11expect(result.current.data?.title).toBe('Emma')
TanStack Query137 / 183
Module 15 · Testing queries and mutations
Testing the error path
TSX
1test('shows an error',async()=>{2 vi.spyOn(api,'getBooks').mockRejectedValue(3newError('Server down')4)5renderWithClient(<Catalogue/>)67expect(8await screen.findByText(/could not load books/i)9).toBeInTheDocument()10})
/…/i is a regular expression: match this text, ignoring upper and lower case.
QuizInteractive · disabled in the PDF
With the app’s normal client (retries on), this test times out. Why?
With the app’s normal client (retries on), this test times out. Why?
TanStack Query138 / 183
Module 15 · Testing queries and mutations
Testing a mutation end to end
Act like the person: fill in the form, click, then check what the screen shows. If the mutation invalidates the reviews, the mocked reviews function is called again; make its second answer include the new review.
userEvent (from @testing-library/user-event) types and clicks like a person.
Test behaviour, not the cache. You could assert on client.getQueryData(…), but that ties tests to your keys. Asserting on what the screen shows lets you refactor keys freely.
TanStack Query139 / 183
Module 15 · Testing queries and mutations
Recap
A new QueryClient per test, with retry: false.
Mock your API functions, or the network with MSW to test your fetch code too.
Wait with findBy… and waitFor, never with fixed sleeps.
renderHook with a wrapper tests custom hooks.
Test what the person sees, not the cache’s internals.
FlashcardsInteractive · disabled in the PDF
Why a new client per test?
So cached data from one test can’t leak into another.
Why retry: false in tests?
So failures show at once instead of after several seconds of retries.
What is a mock?
A stand-in for something real, such as a fake API function.
getBy… vs findBy…?
getBy… checks now; findBy… waits for the element to appear.
What does renderHook do?
Runs a hook in a test component and exposes its latest result as result.current.
What is a flaky test?
One that passes or fails at random, often from timing or shared state.
Module 16: Inside the cache
TanStack Query140 / 183
Module 16 · Inside the cache
Four objects do the work
Underneath the hooks, TanStack Query is a framework-free core (the same one behind its Vue, Solid and Svelte versions). Knowing its pieces makes every behaviour in this course predictable.
useQuerya React hook: subscribes, renders
QueryObserverone per hook: options, derived result
Queryone per key: data, state, the running promise
QueryCacheall queries, by key hash
QueryClientthe API you call; also owns the MutationCache
Query
The cache entry for one key: its data, error, status and timers.
Observer
An object watching a query on behalf of one hook, turning its state into the result you read.
Active query
A query with at least one observer.
TanStack Query141 / 183
Module 16 · Inside the cache
One query, one promise: why requests are shared
A Query holds at most one running fetch. When a second observer asks it to fetch while one is running, it gets the same promise. That is the whole of deduplication: it falls out of there being one object per key.
QuizInteractive · disabled in the PDF
Header uses staleTime: 0, Details uses staleTime: 60_000, same key. The data is 10 seconds old and Details remounts. Does it refetch?
Header uses staleTime: 0, Details uses staleTime: 60_000, same key. The data is 10 seconds old and Details remounts. Does it refetch?
TanStack Query142 / 183
Module 16 · Inside the cache
Keys become hashes
The QueryCache stores queries in a map from query hash to Query. The default hash is a JSON string of the key with object fields sorted. That is why keys must be serializable: functions, class instances and Dates don’t survive it the way you expect.
QuizInteractive · disabled in the PDF
You put a Date in a key: ['books', 'new', { since }]. What does the hash contain for it?
You put a Date in a key: ['books', 'new', { since }]. What does the hash contain for it?
TanStack Query143 / 183
Module 16 · Inside the cache
Structural sharing keeps unchanged parts identical
When a refetch returns, TanStack Query compares the new data with the old, deeply. Wherever a part is equal, it keeps the old object. If nothing changed at all, data is the very same reference as before. This is structural sharing, and it is on by default.
TSX
1// Refetch: only book 2's title changed2const before = books.data // [b1, b2, b3]3// …new answer arrives…4const after = books.data // [b1, b2', b3]56after === before // false: the list changed7after[0]=== before[0]// true: same object kept8after[1]=== before[1]// false: changed
QuizInteractive · disabled in the PDF
Each row is a React.memo component (it skips rendering when its props are the same objects). After that refetch, which rows render again?
Each row is a React.memo component (it skips rendering when its props are the same objects). After that refetch, which rows render again?
TanStack Query144 / 183
Module 16 · Inside the cache
Components only render for the fields they read
The result object has many fields: data, isFetching, failureCount… By default, an observer notices which fields your component actually reads (tracked properties) and only re-renders when one of those changes. A component that never reads isFetching doesn’t render when a background fetch starts.
Find the bugInteractive · disabled in the PDF
This component re-renders on every background fetch, though it only shows data. Which line causes it?
TSX
The option behind it.notifyOnChangeProps controls this: leave it unset for tracking (the default), give a list such as ['data', 'error'], or 'all' to render on any change. (v4’s 'tracked' value is gone in v5, since tracking is the default.)
TanStack Query145 / 183
Module 16 · Inside the cache
select runs when data or the function changes
An observer runs select again when the data changes, or when the select function itself is a different function. An inline arrow is a new function every render, so an expensive select should be defined outside the component or wrapped in useCallback.
And its result is shared too. The result of select also goes through structural sharing, so returning a new but equal array doesn’t re-render. The cost to watch is the work inside select, not the render.
TanStack Query146 / 183
Module 16 · Inside the cache
Focus and network come from two managers
"The window regained focus" and "the device is back online" come from focusManager and onlineManager. In browsers they listen to visibilitychange and the online/offline events. In other places, such as React Native, you plug in your own source of truth.
TypeScript
1import{ focusManager }from'@tanstack/react-query'23// e.g. in a desktop shell that reports app focus itself4focusManager.setEventListener((setFocused)=>{5const off = shell.onFocusChange((focused)=>{6setFocused(focused)7})8return off // called to remove the listener9})
shell stands for whatever environment the app runs in.
QuizInteractive · disabled in the PDF
In a test, you want to trigger "window focus" refetching without a real window. What can you call?
In a test, you want to trigger "window focus" refetching without a real window. What can you call?
TanStack Query147 / 183
Module 16 · Inside the cache
Watching the cache from outside React
The QueryCache is an event source. subscribe calls you on every change (a query added, updated, removed), which is how the devtools work. find and findAll look queries up by filter.
Reading is fine; writing is for the client. Use queryClient methods (setQueryData, invalidateQueries…) to change the cache. Poking at Query objects directly bypasses the bookkeeping that keeps observers in step.
TanStack Query148 / 183
Module 16 · Inside the cache
Recap
Hook → QueryObserver → Query → QueryCache, all owned by the QueryClient.
One Query per key holds one promise: that is deduplication. Options like staleTime live on observers.
Keys are hashed to JSON strings; keep them plain.
Structural sharing keeps unchanged objects identical; tracked properties re-render only for fields read.
Define expensive select functions outside render; focus and network come from replaceable managers.
FlashcardsInteractive · disabled in the PDF
What is a QueryObserver?
The object behind one hook: it watches a Query and computes the result with that hook’s options.
Why are requests deduplicated?
There is one Query per key, holding at most one running promise.
What is a query hash?
The key turned into a string (sorted JSON), used to look the query up.
What is structural sharing?
Keeping old objects wherever new data is equal, so unchanged parts keep their identity.
What are tracked properties?
The result fields a component reads; it only re-renders when they change.
What do focusManager and onlineManager do?
Tell TanStack Query when the app is focused or online; both can be replaced.
Module 17: Server rendering and hydration
TanStack Query149 / 183
Module 17 · Server rendering and hydration
Server rendering draws the first screen on the server
So far, the browser got an empty page, ran the JavaScript, and only then started fetching books. With server-side rendering (SSR), a server runs your React components first and sends finished HTML, so the books are on screen before any JavaScript runs. Then React hydrates: it attaches itself to that HTML in the browser and takes over.
Server-side rendering
Running components on a server to produce HTML for each request.
Hydration (React)
React in the browser taking over HTML the server already drew.
Dehydrate
Turn the cache into plain data that can be sent along with the HTML.
Hydrate (cache)
Put that data into the browser’s cache.
Which framework?. Next.js, Remix, TanStack Start, Astro and others all render on a server in their own way. The TanStack Query side is the same everywhere, and that is what this module teaches.
TanStack Query150 / 183
Module 17 · Server rendering and hydration
Prefetch, dehydrate, hydrate
Put in orderInteractive · disabled in the PDF
Put server rendering with TanStack Query in order.
1.Dehydrate the client
2.Components read the data from the cache with no request
3.In the browser, hydrate the state into the client’s cache
4.Make a new QueryClient for this request
5.Render the HTML and send it with the dehydrated state
6.Prefetch the queries the page needs
TanStack Query151 / 183
Module 17 · Server rendering and hydration
The two halves in code
TSX
1// On the server, for each request2asyncfunctionrenderBookPage(id:number){3const queryClient =newQueryClient()4await queryClient.prefetchQuery(bookQuery(id))5const state =dehydrate(queryClient)6return<BookPageid={id}state={state}/>7}89// Shared by server and browser10functionBookPage({ id, state }:Props){11return(12<HydrationBoundarystate={state}>13<BookDetailsid={id}/>14</HydrationBoundary>15)16}
bookQuery(id) returns { queryKey, queryFn }, shared by both sides (module 19).
Fill the blanksInteractive · disabled in the PDF
Send the server’s cache to the browser.
TSX
1const state =(queryClient)23<state={state}>4<BookDetailsid={id}/>5</HydrationBoundary>
TanStack Query152 / 183
Module 17 · Server rendering and hydration
One client per request on the server
A server handles many people at once. A QueryClient made once, at the top of a file, would be shared by all of them: one person’s reading list could end up in another person’s page.
Find the bugInteractive · disabled in the PDF
Sometimes a person sees another person’s reading list. Which line causes it?
TSX
TanStack Query153 / 183
Module 17 · Server rendering and hydration
In the browser, one client per app, kept in state
In code that runs on both sides, don’t make the client at the top of a module (it would be shared on the server), and don’t make it in render (a new one each time). Make it once per component instance with useState’s initializer function.
TSX
1exportfunctionProviders({ children }:{ children:ReactNode}){2const[queryClient]=useState(3()=>4newQueryClient({5defaultOptions:{queries:{staleTime:60_000}}6})7)8return(9<QueryClientProviderclient={queryClient}>10{children}11</QueryClientProvider>12)13}
The function passed to useState runs only on the first render.
TanStack Query154 / 183
Module 17 · Server rendering and hydration
Set a staleTime, or the browser fetches again at once
QuizInteractive · disabled in the PDF
The server prefetched the book and the browser hydrated it. With the default staleTime: 0, what happens when BookDetails mounts in the browser?
The server prefetched the book and the browser hydrated it. With the default staleTime: 0, what happens when BookDetails mounts in the browser?
Retries on the server. On the server, retry defaults to 0: a slow retry loop would hold the whole page back. The browser retries as usual after hydration.
TanStack Query155 / 183
Module 17 · Server rendering and hydration
Failed prefetches are left for the browser
prefetchQuery never throws, and by default dehydrate only includes successful queries. So if the API fails during server rendering, the page still renders, and the browser fetches the query itself (with retries). If a page can’t make sense without the data, use fetchQuery, which throws, and render an error page.
QuizInteractive · disabled in the PDF
The server should answer 404 for a book that doesn’t exist. Which call lets you detect that while rendering?
The server should answer 404 for a book that doesn’t exist. Which call lets you detect that while rendering?
TanStack Query156 / 183
Module 17 · Server rendering and hydration
Streaming, and what not to send
Some frameworks stream HTML: they send what is ready and fill in the rest as data arrives. Recent v5 versions can dehydrate queries that are still pending, so a query started on the server finishes in the browser. Opt in through dehydrate’s shouldDehydrateQuery option, together with defaultShouldDehydrateQuery.
Dehydrated state is public. Everything dehydrated is written into the HTML, where anyone with the page can read it. Only prefetch what this person may see, and never put secrets in query data.
TanStack Query157 / 183
Module 17 · Server rendering and hydration
Recap
Server: new client per request, prefetch, dehydrate, render. Browser: HydrationBoundary fills the cache.
Never share a client across requests; in the browser keep one in useState.
Set a default staleTime above zero so hydrated data isn’t refetched at once.
prefetchQuery swallows errors and failed queries aren’t dehydrated; use fetchQuery when you must know.
Dehydrated data is visible in the page.
FlashcardsInteractive · disabled in the PDF
What is server-side rendering?
Running components on a server to send finished HTML for each request.
What does dehydrate do?
Turns a client’s successful queries into plain data that can travel with the HTML.
What does HydrationBoundary do?
Writes dehydrated queries into the browser’s cache.
Why a new client per server request?
So one person’s data can never appear in another person’s page.
Why set staleTime with SSR?
With 0, hydrated data is stale at once and refetched on mount.
What was HydrationBoundary called before v5?
Hydrate.
Module 18: Offline, network modes and persistence
TanStack Query158 / 183
Module 18 · Offline, network modes and persistence
Offline, queries pause instead of failing
When onlineManager says the device is offline, a query that wants to fetch doesn’t try and fail: it pauses. Its fetchStatus is 'paused', its status is unchanged, and it resumes on its own when the network returns. Retries pause too.
QuizInteractive · disabled in the PDF
Your connection drops while the app is open. Then you open the catalogue, which has nothing cached. What is its query’s state?
Your connection drops while the app is open. Then you open the catalogue, which has nothing cached. What is its query’s state?
TanStack Query159 / 183
Module 18 · Offline, network modes and persistence
networkMode decides what "offline" means for a query
networkMode
Behaviour
'online' (default)
Doesn’t fetch while offline; pauses
'always'
Always fetches, ignoring the network; never pauses
'offlineFirst'
Tries once; if that fails while offline, pauses its retries
QuizInteractive · disabled in the PDF
A query function reads from the browser’s own storage, not the network. Which mode fits?
A query function reads from the browser’s own storage, not the network. Which mode fits?
The browser’s "online" is a guess. In v5, onlineManager assumes it is online at start, and only changes its mind when the browser fires its offline and online events. Those only know whether there is a network connection, not whether your API is reachable: a connected but captive Wi-Fi counts as online, and so does an app opened with no connection at all. There, requests simply fail and retry.
TanStack Query160 / 183
Module 18 · Offline, network modes and persistence
Mutations pause too, and run when back online
A mutation started offline is paused, not failed. When the network returns, paused mutations continue on their own. While paused, isPaused is true; show it so the person knows their change hasn’t been sent yet.
TSX
1const add =useMutation({2mutationKey:['reading-list','add'],3mutationFn: addToReadingList4})56{add.isPaused &&<small>Will be saved when you are back online</small>}
Optimistic updates help here. Combined with an optimistic update (module 12), an offline write appears straight away and is sent later. Remember that if it finally fails, the rollback happens then, possibly minutes later.
TanStack Query161 / 183
Module 18 · Offline, network modes and persistence
Persisting the cache across reloads
The cache lives in memory, so a reload empties it. A persister saves it to storage (such as localStorage) and restores it on start, so the app opens with data even offline. Two small packages provide this.
It replaces QueryClientProvider. The persister takes any storage with getItem, setItem and removeItem, synchronous like localStorage or asynchronous. The older createSyncStoragePersister is deprecated.
TanStack Query162 / 183
Module 18 · Offline, network modes and persistence
gcTime must be at least as long as maxAge
Persisted data is thrown away after maxAge (24 hours by default). But queries are removed from memory after gcTime, and removed queries aren’t saved. If gcTime is shorter than maxAge, the cache is emptied before it can be restored. Set gcTime to the same or longer.
Fill the blanksInteractive · disabled in the PDF
Keep queries for a day so a day-old cache can be restored.
Module 18 · Offline, network modes and persistence
Resuming saved mutations after a reload
Paused mutations are persisted too, but functions can’t be saved to storage. After a reload, a mutation needs its mutationFn back before it can run. Register it by mutationKey with setMutationDefaults, and resume once the cache is restored.
onSuccess here runs once the saved cache has been restored.
QuizInteractive · disabled in the PDF
Why must setMutationDefaults run before the cache is restored, rather than inside a component?
Why must setMutationDefaults run before the cache is restored, rather than inside a component?
TanStack Query164 / 183
Module 18 · Offline, network modes and persistence
Choose what to save
localStorage is readable by any script on the page and stays after the person leaves. Don’t persist private or sensitive data on shared computers, and clear the cache when someone signs out.
TSX
1persistOptions={{2 persister,3 dehydrateOptions:{4 shouldDehydrateQuery:(query)=>5 query.state.status ==='success'&&6 query.meta?.persist ===true7}8}}910// and on sign-out:11queryClient.clear()12persister.removeClient()
Only queries marked with meta: { persist: true } are saved.
QuizInteractive · disabled in the PDF
Someone signs out on a shared library computer. What should happen to the cache?
Someone signs out on a shared library computer. What should happen to the cache?
TanStack Query165 / 183
Module 18 · Offline, network modes and persistence
Recap
Offline, queries and mutations pause (fetchStatus: 'paused', isPaused) and resume when back online.
networkMode: online (default), always, or offlineFirst.
Persist with PersistQueryClientProvider and a persister; keep gcTime at least maxAge; use buster on releases.
Restored mutations need setMutationDefaults, then resumePausedMutations.
Persist only what is safe; clear memory and storage on sign-out.
FlashcardsInteractive · disabled in the PDF
What does a query do offline by default?
Pauses (fetchStatus: 'paused') and resumes when back online.
What does networkMode: 'always' do?
Fetches regardless of the network; never pauses.
What is a persister?
Something that saves the cache to storage and restores it on start.
Why must gcTime be at least maxAge?
Queries removed from memory aren’t saved, so the cache would empty before it could be restored.
What does buster do?
Discards persisted caches saved with a different buster string, such as an older release.
Why setMutationDefaults for offline writes?
Functions can’t be saved, so restored mutations find their mutationFn by key.
Module 19: Query options and key factories at scale
TanStack Query166 / 183
Module 19 · Query options and key factories at scale
The same query, written five times, drifts
By now one book is fetched from the details page, a hover prefetch, a route loader, the server renderer and a test. If each writes its own { queryKey, queryFn }, sooner or later one says ['book', id] and another ['books', 'detail', id], and the prefetch fills an entry nobody reads.
QuizInteractive · disabled in the PDF
The hover prefetch uses ['book', id]; the page uses ['books', 'detail', id]. What happens on click?
The hover prefetch uses ['book', id]; the page uses ['books', 'detail', id]. What happens on click?
TanStack Query167 / 183
Module 19 · Query options and key factories at scale
queryOptions defines a query once, with its types
queryOptions takes the same object as useQuery and returns it unchanged, but typed: the key now carries the type of the data stored under it. Pass the result anywhere a query is expected.
1useQuery(bookQuery(id))2useSuspenseQuery(bookQuery(id))3queryClient.prefetchQuery(bookQuery(id))45const book = queryClient.getQueryData(bookQuery(id).queryKey)6// ^ typed as Book | undefined, with no <Book> needed
TanStack Query168 / 183
Module 19 · Query options and key factories at scale
A query factory per feature
Group a feature’s keys and options in one object. Each level builds on the one above, so prefixes line up by construction and every group you might invalidate has a name.
Lines 3: A group key, only used for invalidating: every list.
Lines 4-8: A full query: its key extends the group’s.
Lines 9-14: The same for details.
Lines 15-19: Reviews nest under a book’s detail key, so invalidating a book covers its reviews.
TanStack Query169 / 183
Module 19 · Query options and key factories at scale
Invalidation reads like the sentence you mean
TypeScript
1// "every book list"2queryClient.invalidateQueries({queryKey: bookQueries.lists()})34// "book 42 and everything under it"5queryClient.invalidateQueries({6queryKey: bookQueries.detail(42).queryKey7})
QuizInteractive · disabled in the PDF
After editing book 42’s title, which call refreshes its details, its reviews, and every list showing it, and nothing else?
After editing book 42’s title, which call refreshes its details, its reviews, and every list showing it, and nothing else?
TanStack Query170 / 183
Module 19 · Query options and key factories at scale
Hooks only work in components, and fix one choice of hook. Options work everywhere (loaders, the server, tests) and with any hook, and can be extended with spread. Define options; add hooks only where they add something, such as combining several queries.
Fill the blanksInteractive · disabled in the PDF
Reuse the shared options, but select only the title.
TSX
1const title =useQuery({2,3:(book)=> book.title4})
TanStack Query171 / 183
Module 19 · Query options and key factories at scale
Organise by feature
src/
api.ts# getJson, HttpError
query-client.ts# the client and its defaults
books/
api.ts# getBook, searchBooks, getReviews
queries.ts# bookQueries
mutations.ts# useAddReview…
reading-list/
api.ts
queries.ts# readingListQueries
mutations.ts
Everything about one kind of data in one folder.
Mutations next to their queries. A mutation in reading-list/mutations.ts can import readingListQueries and bookQueries to invalidate exactly what it changed, without guessing at keys.
TanStack Query172 / 183
Module 19 · Query options and key factories at scale
Typing errors and meta for the whole app
TanStack Query v5 can be told your app’s error and meta types once, through its Register interface. TypeScript’s declaration merging lets you add fields to an interface that a library declared.
Now error is an HttpError everywhere, and meta is checked.
Be honest about errors. Registering HttpError claims every query fails with one. A query function that can throw something else (a bug, a parse error) breaks that promise. Keep Error as the default if you aren’t sure, and narrow with instanceof.
TanStack Query173 / 183
Module 19 · Query options and key factories at scale
Let a linter check the rules
A linter reads your code and flags likely mistakes. @tanstack/eslint-plugin-query knows TanStack Query’s rules: exhaustive-deps (everything the query function uses is in the key), stable-query-client (no client made during render), no-rest-destructuring, and more.
Find the bugInteractive · disabled in the PDF
exhaustive-deps flags this query. Which line?
TypeScript
TanStack Query174 / 183
Module 19 · Query options and key factories at scale
Recap
Define each query once with queryOptions; use it in hooks, prefetches, loaders, the server and tests.
A query factory per feature builds keys from the level above, so every group has a name.
Prefer shared options over custom hooks; spread them to add select or other per-use options.
Organise by feature; register error and meta types once.
Use @tanstack/eslint-plugin-query to catch missing key inputs and unstable clients.
FlashcardsInteractive · disabled in the PDF
What does queryOptions do?
Returns query options unchanged but typed, so the key carries the data type.
What is a query factory?
An object per feature that builds every key and query option for it.
Why options over custom hooks?
Options work outside components and with any hook; hooks only in components.
What is Register for?
Telling TanStack Query your app’s default error and meta types.
What does exhaustive-deps check?
That every value the query function uses is in the key.
What is declaration merging?
TypeScript adding your fields to an interface someone else declared.
Module 20: Mastery challenge
TanStack Query175 / 183
Module 20 · Mastery challenge
Predict, debug, choose
Each question mixes several modules. Work your answer out before choosing, and read every explanation, including for the ones you got right. Then build the checklist: it is the fastest way to make the cache feel obvious.
Build the book catalogueInteractive · disabled in the PDF
TanStack Query176 / 183
Module 20 · Mastery challenge
Predict: a morning in the cache
Predict the outputInteractive · disabled in the PDF
Every component uses useQuery({ queryKey: ['genres'], queryFn, staleTime: 30_000 }), other options default. How many requests, and what does C show first?
Text
10:00 A mounts20:10 B mounts30:40 the window regains focus40:50 A and B unmount56:00 C mounts
Every component uses useQuery({ queryKey: ['genres'], queryFn, staleTime: 30_000 }), other options default. How many requests, and what does C show first?
TanStack Query177 / 183
Module 20 · Mastery challenge
Predict: what an invalidation touches
QuizInteractive · disabled in the PDF
On screen: ['books', 'list', { search: '' }]. Cached but inactive: ['books', 'list', { search: 'dune' }] and ['books', 'detail', 7]. You call invalidateQueries({ queryKey: ['books', 'list'] }). Which refetch right away?
On screen: ['books', 'list', { search: '' }]. Cached but inactive: ['books', 'list', { search: 'dune' }] and ['books', 'detail', 7]. You call invalidateQueries({ queryKey: ['books', 'list'] }). Which refetch right away?
TanStack Query178 / 183
Module 20 · Mastery challenge
Debug: the list that flashes empty
Find the bugInteractive · disabled in the PDF
After the first keystroke in the search box, the results never refetch when you come back to the tab, and invalidating them from elsewhere in the app does nothing. Which line causes it?
TSX
TanStack Query179 / 183
Module 20 · Mastery challenge
Debug: the rollback that rolls back too far
QuizInteractive · disabled in the PDF
You remove books A and B from your reading list quickly, both optimistically, each with a whole-list snapshot. The request for A fails; B succeeds. Before the final refetch, what does the list show?
You remove books A and B from your reading list quickly, both optimistically, each with a whole-list snapshot. The request for A fails; B succeeds. Before the final refetch, what does the list show?
TanStack Query180 / 183
Module 20 · Mastery challenge
Debug: the page that fetches twice
QuizInteractive · disabled in the PDF
The server prefetches bookQueries.detail(id) and the page hydrates it, yet the Network panel shows GET /books/42 right after load. Which are possible causes?
The server prefetches bookQueries.detail(id) and the page hydrates it, yet the Network panel shows GET /books/42 right after load. Which are possible causes?
TanStack Query181 / 183
Module 20 · Mastery challenge
Choose: the right tool for each job
QuizInteractive · disabled in the PDF
After liking a review, the server answers with the updated review, including its new like count. The review appears in one list. Best update?
After liking a review, the server answers with the updated review, including its new like count. The review appears in one list. Best update?
QuizInteractive · disabled in the PDF
A "new arrivals" strip must update every minute while the page is visible, and not when it is in a background tab. Which options?
A "new arrivals" strip must update every minute while the page is visible, and not when it is in a background tab. Which options?
TanStack Query182 / 183
Module 20 · Mastery challenge
Write it: a review mutation done right
Fill the blanksInteractive · disabled in the PDF
Post a review, stay pending until the book and its reviews have refetched, and keep keys in the factory.
TSX
1const post =({2mutationFn:(review:NewReview)=>postReview(id, review),3onSuccess:()=>4.invalidateQueries({5queryKey: bookQueries.(id).queryKey6})7})
TanStack Query183 / 183
Module 20 · Mastery challenge
Recap: the whole course
Server state is a copy; TanStack Query caches it under keys and refetches when it may be stale.
Every input in the key; one client per app (and per server request); options defined once with queryOptions.
staleTime decides refetching, gcTime decides keeping; triggers are mount, focus, reconnect and intervals.
Writes are mutations: invalidate what they change, write what the server returns, or update optimistically with rollback.
Prefetch to avoid waterfalls; Suspense and error boundaries handle loading and failure by region.
Test with a fresh client and no retries; hydrate server data with a staleTime; pause and persist for offline.
FlashcardsInteractive · disabled in the PDF
The three questions to ask of any query?
What is its key? How long is it fresh? What invalidates it?
Invalidate or setQueryData?
Write what the server sent back; invalidate what the server computes.
Why useState(() => new QueryClient())?
One client per component instance: never per render, never shared between server requests.
Why queryOptions?
One definition of key, function and options, typed, usable everywhere.
What causes a suspense waterfall?
Several useSuspenseQuery calls in one component; each waits for the one before.
Which data do mount, focus and reconnect refetch: fresh or stale?
Stale data. Fresh data is trusted until staleTime has passed.
Answer key
1. Servers, APIs and JSON · An API is the list of questions a server accepts · QuizThe app wants to show the details of book 7. Which request fits?B. GET /books/7/books/7 names one book, and GET means "give me". GET /books would send the whole list, so the app would download far more than it needs. POST is for sending something new.
1. Servers, APIs and JSON · A URL says where to ask, and with which details · QuizIn /books?search=tolkien&page=3, what does page=3 do?A. It asks for the third page of the results for "tolkien"Everything after ? is the query string: extra details for the same request. Book number 3 would be in the path, as /books/3.
1. Servers, APIs and JSON · The method says what you want to do · QuizYou fill in a form and press "Post review". Which method does the request use?B. POSTPosting a review creates something that didn’t exist, so it is a POST. A GET must never change anything on the server.
1. Servers, APIs and JSON · Every answer starts with a status code · QuizThe app asks for GET /books/99999 and that book doesn’t exist. Which status code should come back?B. 404404 says "there is nothing there". It is not a 500: the server worked perfectly, the book just isn’t there. Trying again won’t help, which matters later when you set up retries.
1. Servers, APIs and JSON · Data travels as JSON · QuizIn the JSON above, what is the value of tags?A. A list of two stringsSquare brackets make an array, so tags is a list holding two separate strings. A list of books comes back the same way: an array of objects like this one.
1. Servers, APIs and JSON · A whole exchange, start to finish · Put in orderPut loading the book list in order.1. The app sends GET /books to the server 2. The request travels over the network 3. The server looks up the books 4. The server answers 200 with a JSON array 5. The app turns the JSON into a list on screenEvery step between the first and the last takes time, and the network steps can fail. Waiting for, and recovering from, those steps is what the rest of this course is about.
2. Asking from code: fetch and promises · fetch sends a request and returns a promise · QuizWhat appears in the console first?B. sentfetch only starts the request. JavaScript moves straight on to console.log('sent'). The functions inside .then run later, once the response has arrived.
2. Asking from code: fetch and promises · fetch doesn’t fail on a 404 · Find the bugBook 99999 doesn’t exist. This function "succeeds" with the server’s error message as if it were a book. Which line is wrong?Line 4: if (!response.ok) throw new Error('Failed')Only a 500 is treated as failure, so a 404 falls through and its body is returned as if it were data. response.ok covers every code outside 200 to 299.
2. Asking from code: fetch and promises · Catching errors with try and catch · QuizWhich of these makes fetch itself reject (before you check response.ok)?A. The laptop is offlineWith no network there is no answer at all, so fetch rejects. Every other case is a real response; only your response.ok check turns 404 and 500 into errors.
2. Asking from code: fetch and promises · Sending data: method, headers and body · Fill the blanksRemove a book from the reading list.1: method, 2: DELETE, 3: okThe method goes in the options object. A delete has no body to send. Checking ok is just as important for writes as for reads.
2. Asking from code: fetch and promises · Try it: send a review · Code challengeFinish postReview(bookId, review). POST the review as JSON (with a Content-Type: application/json header) to https://api.example.com/books/<bookId>/reviews. If the response isn’t ok, throw an Error; otherwise return the parsed JSON body.One solution:
JSON.stringify turns the object into the exact text the test expects, and the header tells the server how to read it. The ok check is what makes a 500 reject the promise: without it, fetch fulfils and the error’s body is read as if it were data. That rejection is what a mutation (or a query) reads as failure.
3. Fetching inside a React component · State is what a component remembers · QuizYou type a letter in the box. What happens?A. setSearch stores the new text and React renders SearchBox againCalling the setter is how you tell React something changed. It stores the value and renders again, and on that render useState returns the new text.
3. Fetching inside a React component · An effect runs after the screen updates · QuizWhen does this effect run?A. Once, when the component first appears, and after every render where search changedReact runs an effect after the first render, then compares each dependency with its previous value and runs it again only when one changed. An empty array [] means "only after the first render".
3. Fetching inside a React component · And a third state for when it fails · Find the bugThis component sends requests over and over, forever. Which line causes it?Line 3: useEffect(() => { getJson<Book[]>('/books').then(setBooks) }, [])The request starts during render. When it arrives, setBooks renders again, which starts another request, and so on. Starting work outside rendering is exactly what useEffect is for.
3. Fetching inside a React component · Fetching again when the input changes · Fill the blanksRefetch whenever the search text changes.1: searchAnything from the component that the effect reads belongs in the dependency array. Leave search out and the list never changes as you type.
3. Fetching inside a React component · Count what one list costs · QuizYou open a book’s page, then press Back to the catalogue. What does this Catalogue do?A. Shows "Loading…" and fetches the whole list again from scratchState lives inside the component, and the component was removed when you left. Coming back creates a fresh one with fresh, empty state. Nothing remembers the books between screens.
4. Why fetching gets hard, and what a cache is · Answers can arrive in the wrong order · QuizWith the effect from the last module, which results stay on screen?B. The ones for "du"Each answer calls setBooks when it arrives, so the last one to arrive wins, whichever search it belonged to.
4. Why fetching gets hard, and what a cache is · What you fetched starts going out of date at once · QuizYou post a review with fetch. The Reviews component fetched the list in its effect when the page opened. What does it show now?A. The old list, without your reviewIts effect ran once, when it appeared, and its dependencies haven’t changed. Nothing told it that the server’s list changed. You would need to pass a "refetch" function around, or share state between them.
4. Why fetching gets hard, and what a cache is · Server state is not like other state · QuizWhich are server state?A. The reviews of a book; C. How many books are on your reading listReviews and the reading list are stored on the server and can change without you. Whether a form is open, and an unsent draft, exist only in this browser: client state.
4. Why fetching gets hard, and what a cache is · TanStack Query is that cache, for React · QuizWho actually sends the HTTP request when you use TanStack Query?A. Your own function, which TanStack Query calls when it decides toYou hand it a function that returns a promise (like getJson). TanStack Query decides when to call it and what to do with the result; it never decides how to fetch.
5. Setting up and your first query · The provider makes the client reachable everywhere · Fill the blanksGive the whole app the client.1: QueryClientProvider, 2: client, 3: queryClientThe provider takes the client through its client prop. Every useQuery below App finds it there. If a component outside the provider calls useQuery, it throws "No QueryClient set, use QueryClientProvider to set one".
5. Setting up and your first query · Which part does what · QuizWhich part decides where the answer is stored in the cache?A. queryKeyTanStack Query never looks inside your function, so it doesn’t know or care about URLs. The key alone names the entry. That is why the key must change whenever the data would, as the next module shows.
5. Setting up and your first query · Which part does what · QuizBefore the first answer arrives, what is books.data?A. undefinedThere is no data yet, so data is undefined and isPending is true. Checking isPending, then isError, is what lets TypeScript treat data as defined afterwards: a query whose first fetch failed has no data either.
5. Setting up and your first query · Same key, one request · QuizCatalogue and BookCount appear on the same page at once. How many GET /books requests are sent?A. OneThe second useQuery finds the request for ['books'] already running and waits for the same promise. That is the deduplication you had to build by hand before.
5. Setting up and your first query · Reading the first errors you will meet · Find the bugThis query logs "Query data cannot be undefined". Which line is the problem?Line 4: return getJson<Book>('/books/42')With curly braces, an arrow function needs return. Without it, the promise of the book is thrown away and the function returns undefined. If a request truly has nothing to return, return null instead.
6. Query keys · Every input to the query belongs in the key · Find the bugOpen book 1, then book 2: the page still shows book 1. Which line is wrong?Line 3: queryKey: ['book', id],Both books are stored under ['book'], so the entry for book 1 is found and shown. With id in the key, book 2 gets its own entry and its own request.
6. Query keys · When the key changes, the query changes · QuizYou search "dune", then "emma", then "dune" again. How many requests for "dune" are made?A. One, then the second time it shows from the cache (and may refetch quietly)The "dune" entry is still in the cache, so it shows at once, with no spinner. Whether it also refetches in the background depends on how fresh it is, the subject of module 8.
6. Query keys · Keys are compared by value · QuizWhich keys point at the same entry as ['books', { search: 'dune', page: 2 }]?A. ['books', { page: 2, search: 'dune' }]; D. ['books', { search: 'dune', page: 2, sort: undefined }]Object fields are sorted before hashing, so the first is equal, and a field that is undefined is dropped like in JSON, so the last is too. Moving 'books' changes the array’s order, and '2' is a string, not the number 2.
6. Query keys · The signal cancels requests nobody wants · Fill the blanksLet TanStack Query cancel searches you have typed past.1: signal, 2: signalfetch accepts a signal option. Without it, old searches still finish (each answer lands in its own entry, so the race from module 4 is already fixed), but they waste the network.
6. Query keys · Write keys in one place · QuizWhich keys start with bookKeys.detail(42)?A. bookKeys.reviews(42)['books', 'detail', 42, 'reviews'] begins with ['books', 'detail', 42]. That is why reviews nest under the detail key: acting on book 42 can include its reviews.
6. Query keys · Try it: a key factory · Code challengeFinish bookKeys. lists() is ['books', 'list'], the parent of every list. list(search) adds { search } to it. detail(id) is ['books', 'detail', id], and reviews(id) adds 'reviews' to that.One solution:
Each key starts with its parent's parts, so one prefix names a useful group: lists() for every search at once (after adding a book, say), detail(42) for book 42 and its reviews. as const keeps each part's exact type instead of widening the array to (string | number)[]. A typo in one hand-written key would silently make a separate group; the factory makes that impossible.
7. Loading, errors and retries · fetchStatus says whether a request is running · QuizThe catalogue is on screen and you switch back to its tab, which refetches it. What are status and fetchStatus during that refetch?A. 'success' and 'fetching'The books from the first fetch are still there, so status stays success, while a new request runs, so fetchStatus is fetching. The two answer different questions.
7. Loading, errors and retries · isLoading is the very first load · QuizYou want a small "Updating…" note while the list refreshes in the background, with the list still visible. Which condition fits?A. isFetching && !isPendingThere is data (!isPending) and a request is running (isFetching). isLoading is only ever true when there is no data, so it would never show during a refresh.
7. Loading, errors and retries · A query only fails if its function throws · Find the bugThe server is down and answers 500, but the query shows isSuccess with the error’s body as data. Which line is wrong?Line 5: if (!response.ok) throw new Error(Failed: ${response.status})Logging isn’t failing. The function carries on and returns the error body, so the query succeeds with nonsense. throw rejects the promise, which is what TanStack Query reads as an error.
7. Loading, errors and retries · Try it: a query function that fails properly · Code challengeFinish getReadingList. When the response isn’t ok, throw an Error whose message includes the status code, so the query fails instead of succeeding with an error body. Keep passing signal to fetch.One solution:
Solution
1typeEntry={ bookId:number; read:boolean}2const API ='https://api.example.com'34asyncfunctiongetReadingList({ signal }:{ signal:AbortSignal}){5const response =awaitfetch(`${API}/reading-list`,{ signal })6if(!response.ok){7thrownewError(`Reading list failed: ${response.status}`)8}9return(await response.json())asEntry[]10}
The server sent valid JSON with its 500, so without the check response.json() succeeds and the query is a success holding { error: … }. Throwing rejects the promise, and only that makes TanStack Query retry and then show isError. Putting the status in the message makes the error useful on screen and in logs.
7. Loading, errors and retries · Failed queries are retried, three times by default · QuizWith the defaults, the server is down. Roughly when does the query switch to isError?A. After about 7 seconds and 4 attemptsOne attempt, then 3 retries after waits of about 1, 2 and 4 seconds: 4 attempts, about 7 seconds. That is why a broken endpoint looks like a long spinner during development.
7. Loading, errors and retries · Don’t retry what can’t succeed · Fill the blanksRetry up to 3 times, but never on a 4xx.1: retry, 2: false, 3: 3defaultOptions.queries sets options for every query. The function is asked after each failure and returns whether to try again. instanceof checks which kind of error it is.
7. Loading, errors and retries · A failed refresh keeps the data you had · QuizThe list showed fine; a refetch on tab focus fails after its retries. With the component from "Show every state, in order", what does the person see?A. The error message, even though the old list is still in dataisError is checked before data, so the error wins. That is often too harsh. Checking if (books.isError && !books.data) instead shows the full error only when there is nothing to show, and a small "couldn’t refresh" note otherwise.
8. Freshness, refetching and garbage collection · Stale queries refetch on certain events · QuizWith the defaults, you open the catalogue, switch to your email for a minute, and come back. What happens?A. The list shows at once, and GET /books runs again in the backgroundThe data went stale immediately (staleTime: 0), and focusing the window is a trigger. The old list stays visible and is swapped for the new one when it lands. Many people are surprised by this the first time they see it in the Network panel.
8. Freshness, refetching and garbage collection · Predict the requests · QuizEvery component uses the key ['genres'] with staleTime: 60_000 (one minute). How many requests are made?A. 2The first mount fetches (1). At 0:20 and 0:50 the data is under a minute old: fresh, no request. By 1:30 it is stale, and focus refetches (2). 60_000 is just 60000: underscores make long numbers readable.
8. Freshness, refetching and garbage collection · Try it: the staleness rule · Code challengeWrite isStale(state, staleTime, now). Return true when state.data is undefined, or when at least staleTime milliseconds have passed since state.dataUpdatedAt; otherwise false.One solution:
Freshness is about the age of the data, now - dataUpdatedAt, compared with staleTime. With the default of 0, any age is old enough, so data is stale the moment it arrives; with Infinity, no age is. Note that staleness is only a flag: being stale fetches nothing by itself, it just lets the next trigger (a mount, focus, reconnect) refetch.
8. Freshness, refetching and garbage collection · staleTime and gcTime answer different questions · QuizDefaults everywhere. You leave the catalogue for a book page and return after 2 minutes. What happens?A. The old list shows at once, and refetches in the backgroundTwo minutes is within gcTime, so the data is still cached and shows instantly. It is stale (staleTime: 0), and mounting is a trigger, so it refetches quietly. After 6 minutes it would have been removed, and you would see the spinner.
8. Freshness, refetching and garbage collection · Polling, and refetching by hand · Fill the blanksGenres never change while the app is open. Never refetch them on mount or focus.1: staleTime, 2: InfinityWith an infinite staleTime the data never goes stale on its own, so no trigger refetches it. You can still refetch it by hand or invalidate it.
9. Parallel, dependent and shaped queries · Queries in one component run in parallel · QuizThe book takes 300 ms and the reviews 500 ms. About how long until both are loaded?A. 500 msBoth start together, so the total is the longer of the two. 800 ms would be one after the other, which is a waterfall: the subject of this module’s last slides.
9. Parallel, dependent and shaped queries · enabled: false holds a query back · QuizWhile the book is still loading, what are the author query’s isPending and isLoading?A. isPending true, isLoading falseIt has no data, so it is pending. It isn’t fetching, so it isn’t loading. Using isPending for a spinner here is right; isLoading would show nothing while it waits.
9. Parallel, dependent and shaped queries · skipToken: switch off, with better types · Fill the blanksOnly fetch the author once the id is known.1: skipToken, 2: skipToken, 3: authorIdWhen the query function is skipToken, the query behaves as if enabled were false. In the other branch TypeScript has already narrowed authorId to a number, so no ! is needed.
9. Parallel, dependent and shaped queries · select reshapes the data for one component · QuizA refetch returns the reading list with one book’s note edited, but the same number of books. Does ReadingListCount render again?A. No: its selected value, the count, didn’t changeThe component subscribes to the result of select. The count is the same number, so there is nothing to re-render. Other components showing the notes do render.
9. Parallel, dependent and shaped queries · initialData seeds the cache from what you already have · QuizSearch results have a book’s title only, but the details page also shows its description. Which should you use while the details load?A. placeholderDataThe copy is incomplete. As initialData it would be stored as the book, with no description, and might count as fresh. As a placeholder it fills the page until the real details arrive, then disappears.
9. Parallel, dependent and shaped queries · Waterfalls: requests that wait for no reason · Find the bugThe reviews don’t start loading until the book has loaded, though they only need the id. Which line causes it?Line 6: Render <Reviews bookId={id} /> outside the pending check, or start its query in BookPageThe early return means Reviews (and its useQuery) doesn’t exist until the book is loaded. Render Reviews regardless, or call both queries in BookPage, and they run in parallel. Module 14 shows prefetching as another fix.
10. Mutations: writing to the server · Queries read; mutations write · QuizWhy are mutations not retried by default?A. Repeating a write might do it twice, such as posting the same review two timesA read is safe to repeat; a write might not be. If the first attempt reached the server but the answer was lost, a retry would create a duplicate. You can opt in with retry when you know a write is safe to repeat.
10. Mutations: writing to the server · A mutation has its own status · Fill the blanksDisable the button and change its label while the review is being posted.1: isPending, 2: isErrorDisabling the button while pending stops a second click from posting the review twice. post.variables also holds what is being sent, which module 12 uses.
10. Mutations: writing to the server · Return the invalidation to stay pending until it is done · QuizYou call invalidateQueries in onSuccess without returning it. The form clears when isPending turns false. What might the person see?A. The form clears a moment before their review appears in the listWithout the return, the mutation succeeds as soon as the POST answers, while the reviews are still refetching. Returning the promise makes "done" mean "done and visible".
10. Mutations: writing to the server · Callbacks: on the hook and on each call · Put in orderA mutation succeeds. Put its callbacks in the order they run.1. onMutate on the hook 2. The mutation function runs 3. onSuccess on the hook 4. onSettled on the hook 5. onSuccess on the mutate callThe hook’s callbacks run first, so the cache is updated before the component’s. Callbacks on mutate only run if the component is still mounted: closing a dialog in one is safe, but cache updates there could be skipped.
10. Mutations: writing to the server · mutateAsync when you need to wait · Find the bugWhen the server rejects the review, the console shows "Uncaught (in promise)". Which line is the cause?Line 3: post.mutate(draft) (and read post.isPending instead of saving)mutateAsync rejects when the write fails, and nothing catches it, so the error escapes and setSaving(false) never runs. mutate with isPending needs neither the extra state nor a try.
11. Invalidation and writing to the cache · Invalidating marks stale, then refetches what is on screen · QuizYou add a book to your reading list from its details page. The reading list page isn’t open, but its query is cached. You invalidate ['reading-list']. When is it refetched?A. When you next open the reading list pageFetching data nobody is looking at would be waste. It is marked stale, so the moment the reading list mounts, it refetches (showing the old copy meanwhile).
11. Invalidation and writing to the cache · Ask: what did this write change? · QuizPosting a review changes the reviews and the book’s average rating, shown in its details and in every list. Which invalidation covers it with the fewest refetches?C. ['books', 'detail', id] and ['books', 'list']The detail key is a prefix of the reviews key, so ['books', 'detail', id] covers both. Lists show the rating too, so ['books', 'list'] is needed. ['books'] also works, but it marks every other cached book’s details stale too, so each refetches the next time it is shown.
11. Invalidation and writing to the cache · Never change cached data in place · Find the bugThe new review is in the cache, but the list on screen never shows it. Which line is wrong?Line 5: return old ? [...old, review] : [review]push changes the existing array and the same array is returned, so data is the very same reference as before: nothing looks changed, and nothing re-renders. [...old, review] builds a new array with the review at the end.
11. Invalidation and writing to the cache · Write or refetch? · QuizYou add a book to the reading list. The server answers 204 No Content, and the list is sorted on the server by date added. What should onSuccess do?A. Invalidate ['reading-list']There is no data in the answer to write, and you would have to repeat the server’s sorting. Invalidating lets the server send the truth.
11. Invalidation and writing to the cache · Reading the cache outside a component · Fill the blanksRead the cached book, if any, without a request.1: getQueryDataUnlike invalidateQueries, getQueryData takes an exact key, not a filter. The <Book> tells TypeScript what is stored there; module 19 shows how queryOptions makes that automatic.
12. Optimistic updates · Optimistic updates show success before the server says so · QuizWhich is the best candidate for an optimistic update?A. Starring a book as a favouriteStarring almost never fails, is easy to undo, and feels sluggish if it waits. A review is often rejected, so showing it as posted and then pulling it back would be confusing. Money should always wait for the server.
12. Optimistic updates · Why cancel queries first · QuizYou skip cancelQueries. A refetch of the reading list started just before the click, and answers 100 ms after your optimistic write. What does the person see?A. The removed book comes back, then disappears again after the final invalidationThat refetch was asked before the delete, so its answer still has the book, and it overwrites your optimistic list. Once the mutation settles and the list refetches, it disappears again: a flicker. Cancelling first throws the stale answer away.
12. Optimistic updates · Why cancel queries first · Put in orderPut a cache-based optimistic update in order.1. Cancel running queries for the key 2. Snapshot the current data 3. Write the optimistic data into the cache 4. The mutation function runs against the server 5. On error, restore the snapshot 6. Invalidate the key so it matches the serverAll of the first three happen in onMutate, before the request is sent. The rollback only runs on failure; the invalidation always runs, in onSettled.
12. Optimistic updates · Take the snapshot before you write · Find the bugWhen the server fails, the book stays removed. Which line is in the wrong place?Line 6: Move the getQueryData line above setQueryDataThe snapshot is taken after the optimistic write, so it already lacks the book. "Restoring" it changes nothing. Read first, then write.
12. Optimistic updates · Optimistic updates inside one item · Fill the blanksMark one entry as read in the cached list.1: map, 2: ...entrymap returns a new array, and { ...entry, read: true } a new object with every field copied and read changed. The other entries are the same objects as before, so components showing them don’t need to change.
12. Optimistic updates · Try it: the updater function · Code challengeWrite markRead(old, bookId). Return a new list where the entry with that bookId is a copy with read: true, and every other entry is the very same object as before. Never change old; if old is undefined, return undefined.One solution:
map builds a new array and { ...entry, read: true } a new object, so the cached data is never touched. TanStack Query decides what changed by comparing references: the starter changed the entry in place and returned the same array, so the cache would see no change and nothing would re-render. Keeping untouched entries as the same objects lets their components skip rendering.
12. Optimistic updates · New items need a temporary id · QuizWhile the item is pending, the person clicks "Remove" on it. What is the problem?A. Its id is temporary, so the server knows nothing about itActions that need a real id should be disabled on pending items (that is what the pending flag is for) until the refetch brings the real one.
13. Pagination and infinite lists · Numbered pages: the page goes in the key · QuizWhy disable "Next page" while isPlaceholderData is true?A. The shown data is the previous page’s, so hasMore may not describe the page you are onWhile page 3 loads, you are looking at page 2’s data. Waiting for the real page avoids racing ahead on stale information.
13. Pagination and infinite lists · The shape of infinite data · QuizAfter a third page loads (which returns nextCursor: null), what is pageParams?A. ['', 'c20', 'c40']pageParams[i] is the param used to fetch pages[i]. The third page was fetched with c40. Its null cursor means getNextPageParam returns null, so hasNextPage becomes false.
13. Pagination and infinite lists · Loading more · Fill the blanksFinish the "Load more" button.1: fetchNextPage, 2: hasNextPage, 3: isFetchingNextPagefetchNextPage calls the query function with the next param and appends the page. hasNextPage is false once getNextPageParam returns undefined or null. isFetchingNextPage is narrower than isFetching: only the "more" request.
13. Pagination and infinite lists · Refetching an infinite list refetches every page · QuizFive pages are loaded. You invalidate the feed. What happens?A. Five requests, in order, each using the next param from the fresh previous pagePages are refetched in sequence because each next param comes from the page before it, which may have changed. Expect a slow refresh for long lists.
13. Pagination and infinite lists · Choosing between them · QuizA search results page shows 20 books, with numbered page links at the bottom. Which fits?A. useQuery with page in the key and keepPreviousDataEach page replaces the last, and any page can be opened directly. That is one query per page. useInfiniteQuery is for lists that grow as you go.
14. Prefetching, Suspense and error boundaries · Fetch on hover, show on click · QuizYou hover the same link five times within ten seconds. How many requests does the prefetch send?A. OneprefetchQuery only fetches when the data is missing or stale. With staleTime: 60_000, the first answer stays fresh for a minute, so the other hovers do nothing.
14. Prefetching, Suspense and error boundaries · Suspense queries in one component run one after another · QuizEach request takes 1 second. Roughly how long until BookPage shows?A. About 2 secondsThe first hook suspends, so the component stops there: the second query doesn’t even start until the first has data and React renders again. Use useSuspenseQueries, prefetch both, or put each in its own component.
14. Prefetching, Suspense and error boundaries · Suspense queries in one component run one after another · Fill the blanksFetch both in parallel and still suspend.1: useSuspenseQueries, 2: queriesuseSuspenseQueries starts every query in the same render and suspends until all have data. bookQuery(id) stands for a function returning the options object; module 19 builds those.
14. Prefetching, Suspense and error boundaries · "Try again" needs a reset · Find the bugClicking "Try again" shows the error again instantly, with no request. Which line is missing something?Line 1: Wrap in QueryErrorResetBoundary and pass onReset={reset}The boundary resets, renders BookDetails, and the query, still in error, throws again. Telling the query cache to reset first makes it retry.
14. Prefetching, Suspense and error boundaries · Keep the old page while the new key suspends · QuizA component must show a book, and its data is guaranteed by a loader that already ran. Which hook gives the simplest component?A. useSuspenseQueryThe data is (almost) always there, and useSuspenseQuery types data as defined, so the component needs no loading branch. If the cache was empty after all, it suspends rather than breaking.
15. Testing queries and mutations · A fresh client for every test · Find the bugEach test passes alone, but "shows an error" fails when run after "shows the books". Which line causes it?Line 1: Create the client inside renderBooks, with retry: falseOne client is shared by every test, so the second test finds the first test’s books in the cache and never sees its own failing request. A client per test isolates them.
15. Testing queries and mutations · Replace the network · QuizWhat does mocking at the network level (with MSW) test that mocking getBooks doesn’t?A. Your getJson code: the URL, the response.ok check and JSON parsingWith getBooks mocked, everything inside it is skipped. With the network mocked, only the server is fake, so a wrong URL or a missing ok check would show up in the test.
15. Testing queries and mutations · Wait for what the person would wait for · QuizReplacing await screen.findByText('Emma') with screen.getByText('Emma') makes the test fail. Why?A. getBy… checks right now, while the component still shows "Loading…"The data arrives a moment after the first render. findBy… retries until the element appears; getBy… looks once. Never add fixed sleeps instead: they make tests slow and flaky.
15. Testing queries and mutations · Testing a custom hook · Fill the blanksRender the hook inside a provider and wait for success.1: renderHook, 2: wrapper, 3: waitForresult.current is the hook’s latest return value. Waiting on isSuccess rather than on data also makes a failure obvious: the test times out on a clear condition.
15. Testing queries and mutations · Testing the error path · QuizWith the app’s normal client (retries on), this test times out. Why?A. The query retries 3 times with backoff, so the error appears after about 7 seconds, past findBy…’s waitThis is the most common TanStack Query testing surprise. retry: false in the test client makes the first failure final.
16. Inside the cache · One query, one promise: why requests are shared · QuizHeader uses staleTime: 0, Details uses staleTime: 60_000, same key. The data is 10 seconds old and Details remounts. Does it refetch?A. No: Details’ observer uses its own staleTime and sees fresh dataOptions like staleTime and select live on the observer, so each component judges freshness for itself. Header remounting would refetch. Different options on one key are legal but confusing; share them (module 19).
16. Inside the cache · Keys become hashes · QuizYou put a Date in a key: ['books', 'new', { since }]. What does the hash contain for it?A. The date as a string, as JSON.stringify writes itJSON.stringify turns a Date into an ISO string, so two dates for the same moment match. But the key you read back in the query function is still the Date object. Prefer plain strings or numbers in keys; a custom queryKeyHashFn exists for unusual cases.
16. Inside the cache · Structural sharing keeps unchanged parts identical · QuizEach row is a React.memo component (it skips rendering when its props are the same objects). After that refetch, which rows render again?A. Only the row for book 2Rows 1 and 3 receive the same objects as before, so memo skips them. Without structural sharing, every object would be new and every row would render. It only works for JSON-like data, and can be turned off with structuralSharing: false for huge answers where the comparison costs more than it saves.
16. Inside the cache · Components only render for the fields they read · Find the bugThis component re-renders on every background fetch, though it only shows data. Which line causes it?Line 2: const { data } = useQuery(bookQuery(id))...rest reads every field to copy them, so all of them are tracked, including isFetching. The ESLint rule no-rest-destructuring from @tanstack/eslint-plugin-query catches this.
16. Inside the cache · Focus and network come from two managers · QuizIn a test, you want to trigger "window focus" refetching without a real window. What can you call?A. focusManager.setFocused(true) (after false)setFocused overrides the browser’s answer. Going from unfocused to focused makes stale, active queries with refetchOnWindowFocus refetch. Pass undefined to hand control back to the browser.
17. Server rendering and hydration · Prefetch, dehydrate, hydrate · Put in orderPut server rendering with TanStack Query in order.1. Make a new QueryClient for this request 2. Prefetch the queries the page needs 3. Dehydrate the client 4. Render the HTML and send it with the dehydrated state 5. In the browser, hydrate the state into the client’s cache 6. Components read the data from the cache with no requestThe cache travels with the page. As long as the browser’s components use the same keys, they find their data waiting.
17. Server rendering and hydration · The two halves in code · Fill the blanksSend the server’s cache to the browser.1: dehydrate, 2: HydrationBoundarydehydrate makes a plain, serializable object of the successful queries. HydrationBoundary (called Hydrate before v5) writes them into whichever client is in context.
17. Server rendering and hydration · One client per request on the server · Find the bugSometimes a person sees another person’s reading list. Which line causes it?Line 1: Create the client inside renderReadingListThe key ['reading-list'] doesn’t say whose list it is, and the cache outlives the request. When two people’s requests overlap, the second prefetch joins the first one’s running fetch and gets that person’s list, and dehydrate sends whatever the shared cache holds. A new client per request makes each cache private and short-lived.
17. Server rendering and hydration · Set a staleTime, or the browser fetches again at once · QuizThe server prefetched the book and the browser hydrated it. With the default staleTime: 0, what happens when BookDetails mounts in the browser?A. It shows the book and immediately refetches itHydrated data is stale at once with staleTime: 0, and mounting is a trigger. The page is right, but you pay for the request twice. A staleTime above zero (say a minute) as a default avoids it.
17. Server rendering and hydration · Failed prefetches are left for the browser · QuizThe server should answer 404 for a book that doesn’t exist. Which call lets you detect that while rendering?A. await queryClient.fetchQuery(bookQuery(id)) in a tryfetchQuery rejects with the query function’s error (your HttpError with status 404), so you can choose a 404 page. prefetchQuery swallows errors by design.
18. Offline, network modes and persistence · Offline, queries pause instead of failing · QuizYour connection drops while the app is open. Then you open the catalogue, which has nothing cached. What is its query’s state?A. status: 'pending', fetchStatus: 'paused'There is no data (pending) and the fetch is waiting for the network (paused). A spinner checking only isPending would spin forever; check fetchStatus === 'paused' (or isPaused) to say "You are offline" instead.
18. Offline, network modes and persistence · networkMode decides what "offline" means for a query · QuizA query function reads from the browser’s own storage, not the network. Which mode fits?A. 'always'The function doesn’t need the network, so there is no reason to pause it. 'offlineFirst' suits requests that may be answered by a service worker’s cache: try first, pause if that fails.
18. Offline, network modes and persistence · gcTime must be at least as long as maxAge · Fill the blanksKeep queries for a day so a day-old cache can be restored.1: gcTime, 2: maxAge24 hours in milliseconds for both. persistOptions also takes a buster string: change it (with each release, say) to throw away caches saved by older versions whose data shape no longer matches.
18. Offline, network modes and persistence · Resuming saved mutations after a reload · QuizWhy must setMutationDefaults run before the cache is restored, rather than inside a component?A. Restored mutations need their function at once, before any component that defines it has mountedThe page that made the mutation might not even be the one open after the reload. Defaults on the client, set at start-up, make every saved mutation runnable.
18. Offline, network modes and persistence · Choose what to save · QuizSomeone signs out on a shared library computer. What should happen to the cache?A. Clear it in memory and remove the persisted copyInvalidating only marks data stale; it is still there to show. clear() empties memory, and removing the persisted client deletes the saved copy, so the next person sees nothing of the last.
19. Query options and key factories at scale · The same query, written five times, drifts · QuizThe hover prefetch uses ['book', id]; the page uses ['books', 'detail', id]. What happens on click?A. The page fetches again: the prefetched entry is never usedDifferent keys are different entries, silently. Nothing warns you; the prefetch just wastes a request. The cure is to define each query exactly once.
19. Query options and key factories at scale · Invalidation reads like the sentence you mean · QuizAfter editing book 42’s title, which call refreshes its details, its reviews, and every list showing it, and nothing else?A. Invalidate bookQueries.detail(42).queryKey and bookQueries.lists()The detail key is a prefix of the reviews key, and lists() covers every search. all() also works but marks every other book’s details stale. Reviews alone misses the details and lists.
19. Query options and key factories at scale · Custom hooks or shared options? · Fill the blanksReuse the shared options, but select only the title.1: ...bookQuery(id), 2: selectSpreading copies the key, function and staleTime; select is added for this component only. TypeScript knows book is a Book from the options.
19. Query options and key factories at scale · Let a linter check the rules · Find the bugexhaustive-deps flags this query. Which line?Line 3: queryKey: ['books', 'detail', id, 'reviews', { sort }],sort changes the answer but not the key, so newest-first and oldest-first share one entry. Keeping sort inside an object at the end also keeps the reviews prefix intact for invalidation.
20. Mastery challenge · Predict: a morning in the cache · QuizEvery component uses useQuery({ queryKey: ['genres'], queryFn, staleTime: 30_000 }), other options default. How many requests, and what does C show first?A. 3 requests; C shows its loading state firstA fetches (1). B at 0:10 finds fresh data: no request. At 0:40 the data is stale and observed, so focus refetches (2). At 0:50 the query goes inactive; gcTime removes it at 5:50. C at 6:00 finds nothing: it fetches (3) from pending.
20. Mastery challenge · Predict: what an invalidation touches · QuizOn screen: ['books', 'list', { search: '' }]. Cached but inactive: ['books', 'list', { search: 'dune' }] and ['books', 'detail', 7]. You call invalidateQueries({ queryKey: ['books', 'list'] }). Which refetch right away?A. ['books', 'list', { search: '' }]Both lists match the prefix and become stale, but only active queries refetch now; the "dune" list refetches when a search for it next mounts. The detail key doesn’t start with ['books', 'list'], so it isn’t touched at all.
20. Mastery challenge · Debug: the list that flashes empty · Find the bugAfter the first keystroke in the search box, the results never refetch when you come back to the tab, and invalidating them from elsewhere in the app does nothing. Which line causes it?Line 3: const [queryClient] = useState(() => new QueryClient())Typing changes App’s state, so App renders, which makes a brand new, empty client and unmounts the old one. Results keeps the client it started with, which no longer hears focus events, while useQueryClient() everywhere else now hands out the newest one, so invalidations go to a cache that holds none of the results. Components that mount later start from an empty cache. The stable-query-client lint rule flags this.
20. Mastery challenge · Debug: the rollback that rolls back too far · QuizYou remove books A and B from your reading list quickly, both optimistically, each with a whole-list snapshot. The request for A fails; B succeeds. Before the final refetch, what does the list show?A. A and B both back: A’s snapshot was taken before B was removedA’s snapshot is the list with both books. Restoring it brings B back too, until the onSettled invalidation corrects it. Rolling back just the failed item (re-inserting A), or using the variables approach, avoids clobbering other writes.
20. Mastery challenge · Debug: the page that fetches twice · QuizThe server prefetches bookQueries.detail(id) and the page hydrates it, yet the Network panel shows GET /books/42 right after load. Which are possible causes?A. The browser’s query has staleTime: 0; B. The component uses a different key from the prefetch; C. The server’s prefetch failed, so the query wasn’t dehydratedStale hydrated data refetches on mount; a different key finds nothing; a failed prefetch isn’t sent, so the browser fetches it. Hydration itself never fetches. Shared queryOptions with a staleTime rule out the first two.
20. Mastery challenge · Choose: the right tool for each job · QuizAfter liking a review, the server answers with the updated review, including its new like count. The review appears in one list. Best update?A. setQueryData on the reviews list, replacing that review with the answerThe server sent exactly what changed, so writing it in costs no request and is instantly right. Invalidating everything would refetch details and lists that didn’t change.
20. Mastery challenge · Choose: the right tool for each job · QuizA "new arrivals" strip must update every minute while the page is visible, and not when it is in a background tab. Which options?A. refetchInterval: 60_000Interval refetching pauses in the background unless you opt in. staleTime alone only allows a refetch after a minute; it doesn’t cause one.
20. Mastery challenge · Write it: a review mutation done right · Fill the blanksPost a review, stay pending until the book and its reviews have refetched, and keep keys in the factory.1: useMutation, 2: queryClient, 3: detailReturning the invalidation keeps isPending true until the refetch is done. The detail key is a prefix of the reviews key, so one call covers the rating and the reviews. Add bookQueries.lists() too if lists show ratings.