skip to content

When a stale TanStack Query v5 infinite query holding ten pages refetches, what requests does it send, and how does maxPages change that?

level: middleimportance: should knowfreq 42%

answer

  1. not just the first page
  2. one after another, not in parallel
  3. params recomputed from fresh pages
  4. a sliding window of pages
  5. the window needs both directions

basics

~20 s

It refetches all ten pages one after another, starting from the first stored param and computing each next param from the freshly fetched page. maxPages caps how many pages are kept, so a refetch requests at most that many.

solid answer

~50 s

A refetch of an infinite query, after invalidation, a focus refetch or a remount while stale, does not refresh pages independently. TanStack Query starts from `pageParams[0]`, fetches that page, calls `getNextPageParam` on the **fresh** result to get the next param, and repeats until it has as many pages as before or the function returns `undefined` or `null`. The requests run sequentially, so ten pages cost ten round trips, and the new `pages` array replaces the old one only when the whole chain has finished. Recomputing the params is deliberate: cursors stored earlier may be wrong after the list has changed. `maxPages` limits how many pages are stored. Appending past the limit drops the first page and prepending drops the last, so a refetch requests at most `maxPages` pages. Dropped pages must be reachable again, so `maxPages` needs both `getNextPageParam` and `getPreviousPageParam`.

code

tsx · 22 lines
tsx
import { useInfiniteQuery } from '@tanstack/react-query'

type LogPage = { lines: string[]; prevCursor?: number; nextCursor?: number }

async function fetchLogPage({ pageParam }: { pageParam: number }): Promise<LogPage> {
  const res = await fetch(`/api/logs?cursor=${pageParam}`)
  if (!res.ok) throw new Error('Failed to load logs')
  return res.json()
}

export function useLogWindow() {
  return useInfiniteQuery({
    queryKey: ['logs'],
    queryFn: fetchLogPage,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    // Required with maxPages: dropped pages must be fetchable again.
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
    // Keep, and therefore refetch, at most three pages.
    maxPages: 3,
  })
}

go deeper

for a junior

Remember that refetching an infinite list refetches every loaded page, not just the first, and that maxPages limits how many pages are kept.

for a middle

Walk through the refetch loop: start from the first stored param, fetch, compute the next param from the fresh page, repeat, and swap the data in once at the end.

for a senior

Connect slow tab-return refreshes to deep infinite lists, and use maxPages with getPreviousPageParam to bound both memory and refetch chains.

for a principal

Weigh a bounded sliding window against a full history for each list, considering scroll restoration, back-navigation expectations and backend cost per refetch.

## One entry, refetched as a chain `useInfiniteQuery` stores every loaded page under **one query key**, as `data.pages` plus the `data.pageParams` that produced them. When that entry is refetched (because it was invalidated, the window regained focus while it was stale, or a component remounted onto stale data), TanStack Query cannot refresh the pages independently. Each page's param may depend on the page before it, as a cursor does. So the library **re-walks the list from the start**. ## Step by step For a query holding ten pages, a refetch runs this loop: 1. Take the param for the first page from `pageParams[0]`. `initialPageParam` is used only if there is no stored param at all. 2. Call `queryFn` with that param and wait for the page. 3. Call `getNextPageParam(lastPage, allPages, lastPageParam, allPageParams)` on the pages fetched **so far in this refetch**, not on the old data. 4. If it returns `undefined` or `null`, stop early; the list is shorter now. 5. Otherwise fetch the next page and repeat until the refetch has as many pages as the query held before. 6. Replace `data` with the new `pages` and `pageParams` in one update. Three consequences follow: - The requests are **sequential**. Ten pages mean ten round trips, one after the other. - The list on screen does **not** update page by page. The old pages stay visible, with `isFetching` true, until the last request of the chain has finished. - If the entry was **removed from the cache** by garbage collection, there is nothing to re-walk, and the query starts again from `initialPageParam` with only the first page. ## Seeing it in the browser The chain is easy to recognise once you know its shape: - The network tab shows one request per loaded page, each starting only after the previous one has finished, rather than a parallel burst. - During the chain `isFetching` is `true` but `isFetchingNextPage` is `false`, because this is a refetch of the whole list, not a load-more. - The rows on screen do not change until the last request of the chain returns, and then every page updates at once. ## Why the params are recomputed The docs give the reason directly: fetching sequentially from the first page ensures that, even if the underlying data was mutated, stale cursors are not used, which could produce duplicates or skipped records. For a **cursor** API the new `nextCursor` values come from the fresh pages, so the page boundaries follow the current data. For an **offset** API written as `lastPageParam + 1`, the recomputed params equal the old ones, and the whole window is simply fetched again from current data. ## The cost of long lists, and maxPages A user who scrolls through fifty pages pays for it twice: in memory, and in a fifty-request chain on every refetch. v5's **`maxPages`** option turns the list into a **sliding window**: | Action | Pages before (`maxPages: 3`) | Pages after | |---|---|---| | `fetchNextPage()` | 1, 2, 3 | 2, 3, 4 (page 1 dropped) | | `fetchPreviousPage()` | 2, 3, 4 | 1, 2, 3 (page 4 dropped) | | refetch | 2, 3, 4 | 2, 3, 4, re-requested from page 2's param | The matching `pageParams` entry is dropped along with each page, so the arrays stay aligned. A refetch now costs at most `maxPages` requests, and memory stays bounded. ## Caveats - `maxPages` needs **both** `getNextPageParam` and `getPreviousPageParam`. A dropped page has to be fetchable again when the user scrolls back, and the refetch of a window that no longer starts at the first page begins from the first **kept** page's param. - A sliding window changes the rendering contract. Items drop out of `data.pages` as the user scrolls, so a scroll container must keep its position when content is removed above it. Bi-directional lists usually need a virtualized or position-anchored list for this reason. - `maxPages` does **not** stop loading. `hasNextPage` still comes only from `getNextPageParam`. - v4 had a `refetchPage` option for choosing which pages to refetch. v5 removed it, and `maxPages` covers the same need, bounding refetch cost, without the problems `refetchPage` had.

  • A support ticket says a feed takes several seconds to refresh after the user returns to the tab. What would you check first?
    How many pages the feed holds when it refetches. A focus refetch of a stale infinite query re-requests every loaded page in sequence, so a user fifty pages deep waits for fifty round trips before the list updates. Setting `maxPages` bounds the chain. Reconsidering whether that key needs focus refetching at all is a staleness decision for the same query.
  • Why does the refetch start from pageParams[0] rather than from initialPageParam?
    Because the first kept page may not be the start of the list, for example after a `fetchPreviousPage` or when `maxPages` has dropped earlier pages. Starting from the stored first param refetches the window the user is actually looking at. `initialPageParam` is used only when the query has no stored pages.

saying these in an interview costs you the question

  • A refetch only re-requests the first page of an infinite query.
  • Refetching an infinite query fires all loaded pages in parallel.
  • A refetch replays the stored pageParams exactly as they were.
  • maxPages stops the user from loading more pages once reached.
  • maxPages works with getNextPageParam alone.