Skip to the content

Module 1: Servers, APIs and JSON

Module 2: Asking from code: fetch and promises

Module 3: Fetching inside a React component

Module 4: Why fetching gets hard, and what a cache is

Module 5: Setting up and your first query

Module 6: Query keys

Module 7: Loading, errors and retries

Module 8: Freshness, refetching and garbage collection

Module 9: Parallel, dependent and shaped queries

Module 10: Mutations: writing to the server

Module 11: Invalidation and writing to the cache

Module 12: Optimistic updates

Module 13: Pagination and infinite lists

Module 14: Prefetching, Suspense and error boundaries

Module 15: Testing queries and mutations

Module 16: Inside the cache

Module 17: Server rendering and hydration

Module 18: Offline, network modes and persistence

Module 19: Query options and key factories at scale

Module 20: Mastery challenge

Answer key

  1. 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.
  2. 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.
  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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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:
    Solution
    type NewReview = { stars: number; text: string }
    async function postReview(bookId: number, review: NewReview) {  const response = await fetch(    `https://api.example.com/books/${bookId}/reviews`,    {      method: 'POST',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify(review)    }  )  if (!response.ok) throw new Error(`Failed: ${response.status}`)  return response.json()}
    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.
  12. 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.
  13. 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".
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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".
  22. 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.
  23. 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.
  24. 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.
  25. 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.
  26. 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.
  27. 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.
  28. 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.
  29. 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.
  30. 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.
  31. 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:
    Solution
    const bookKeys = {  all: ['books'] as const,  lists: () => ['books', 'list'] as const,  list: (search: string) => ['books', 'list', { search }] as const,  detail: (id: number) => ['books', 'detail', id] as const,  reviews: (id: number) => ['books', 'detail', id, 'reviews'] as const}
    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.
  32. 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.
  33. 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.
  34. 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.
  35. 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
    type Entry = { bookId: number; read: boolean }const API = 'https://api.example.com'
    async function getReadingList({ signal }: { signal: AbortSignal }) {  const response = await fetch(`${API}/reading-list`, { signal })  if (!response.ok) {    throw new Error(`Reading list failed: ${response.status}`)  }  return (await response.json()) as Entry[]}
    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.
  36. 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.
  37. 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.
  38. 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.
  39. 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.
  40. 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.
  41. 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:
    Solution
    type QueryState = { data: unknown; dataUpdatedAt: number }
    function isStale(  state: QueryState, staleTime: number, now: number): boolean {  if (state.data === undefined) return true  return now - state.dataUpdatedAt >= staleTime}
    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.
  42. 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.
  43. 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.
  44. 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.
  45. 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.
  46. 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.
  47. 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.
  48. 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.
  49. 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.
  50. 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.
  51. 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.
  52. 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".
  53. 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.
  54. 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.
  55. 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).
  56. 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.
  57. 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.
  58. 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.
  59. 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.
  60. 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.
  61. 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.
  62. 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.
  63. 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.
  64. 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.
  65. 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:
    Solution
    type Entry = { id: string; bookId: number; read: boolean }
    function markRead(old: Entry[] | undefined, bookId: number) {  return old?.map((entry) =>    entry.bookId === bookId ? { ...entry, read: true } : entry  )}
    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.
  66. 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.
  67. 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.
  68. 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.
  69. 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.
  70. 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.
  71. 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.
  72. 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.
  73. 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.
  74. 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.
  75. 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.
  76. 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.
  77. 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.
  78. 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.
  79. 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.
  80. 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.
  81. 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.
  82. 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).
  83. 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.
  84. 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.
  85. 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.
  86. 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.
  87. 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.
  88. 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.
  89. 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.
  90. 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.
  91. 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.
  92. 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.
  93. 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.
  94. 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.
  95. 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.
  96. 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.
  97. 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.
  98. 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.
  99. 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.
  100. 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.
  101. 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.
  102. 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.
  103. 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.
  104. 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.
  105. 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.
  106. 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.
  107. 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.
  108. 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.