Capítulo 23 de 80

Chapter 23: Infinite Queries

Core Idea

useInfiniteQuery manages a single cache entry holding an ordered array of fetched pages (data.pages + data.pageParams), advanced by fetchNextPage/fetchPreviousPage, with getNextPageParam/getPreviousPageParam deciding whether more data exists and what parameter to request it with.

Key Concepts

  • Shape differences from useQuery: data is { pages, pageParams } instead of a flat value; fetchNextPage/fetchPreviousPage trigger loading more; initialPageParam is required to seed the first request; hasNextPage/hasPreviousPage derive from whether getNextPageParam/getPreviousPageParam return a defined value; isFetchingNextPage/isFetchingPreviousPage distinguish "loading more" from a background refresh.
  • One fetch at a time per query: all pages share a single cache entry, so calling fetchNextPage while a fetch is already in flight risks overwriting a concurrent background refresh — guard calls with !isFetching (e.g. onEndReached={() => hasNextPage && !isFetching && fetchNextPage()}), or opt into concurrent fetches explicitly via fetchNextPage({ cancelRefetch: false }) (default cancelRefetch is true).
  • Refetch order: when a stale infinite query refetches, its pages are refetched sequentially from the first one — this avoids stale cursors producing duplicates/gaps as underlying data mutates. If the query is evicted from the cache entirely, pagination restarts from the initial page.
  • Bi-directional lists: add getPreviousPageParam/fetchPreviousPage/hasPreviousPage/isFetchingPreviousPage alongside the "next" set for lists that load in both directions.
  • Reversing page order for display: use select to return { pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse() } rather than reversing in the render function.
  • Manual cache edits: any manual queryClient.setQueryData write must preserve the { pages, pageParams } shape — e.g. pages: data.pages.slice(1) to drop the first page, always updating both arrays together.
  • maxPages: caps how many pages stay in the cache (used with both getNextPageParam and getPreviousPageParam) — bounds memory usage and how many pages get refetched sequentially when the query goes stale.
  • Cursor-less APIs: getNextPageParam/getPreviousPageParam also receive the current page's own pageParam as a third argument, so you can compute the next one (lastPageParam + 1) even when the API response carries no cursor field itself.

Code Examples

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetch('/api/projects?cursor=' + pageParam).then((r) => r.json()),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Safe "load more" trigger — guards against overlapping in-flight fetches
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
  • What it demonstrates: the minimal infinite-query setup (cursor-based) plus the standard safe-trigger guard for fetchNextPage.

Key Takeaways

  1. Always guard fetchNextPage() calls with !isFetching (or accept the cancelRefetch: false risk deliberately) — a single cache entry means overlapping fetches can overwrite each other.
  2. Use maxPages proactively for lists that can grow to dozens of pages — it bounds both memory and the sequential-refetch cost when the query goes stale.
  3. Manual cache writes to an infinite query must always update pages and pageParams together and keep them the same length/order — partial edits break the pagination model.

Connects To

  • Placeholder Query Data: keepPreviousData applies to infinite queries as well.
  • Query Functions: pageParam arrives via the same QueryFunctionContext covered there.