---
id: infinite-queries
track: learn
product: query
locale: en
order: 15
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Query ^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
prerequisites:
  - "Query keys and query functions"
  - "A backend with stable cursor or explicit page-number semantics"
sourceRefs:
  - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries"
  - "https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery"
---

# Keep infinite lists bounded and recoverable

> Define pagination with initialPageParam, cursors, and maxPages while preserving the pages and pageParams structure.

**Outcome:** Implement a load-more list that avoids duplicate fetches, bounds memory, and detects the final page correctly.

## Infinite data is not a plain array

Infinite-query cache stores both data.pages and data.pageParams. initialPageParam is the required starting point; getNextPageParam returns the next pageParam and null or undefined means the list has ended. Manual cache updates must preserve both arrays and their correspondence.

## Let the server cursor own pagination truth

Take the cursor from the previous server response rather than guessing it on the client. The queryKey describes list filters while pageParam describes the current page position; putting every cursor into the key splits pages into unrelated queries.

File: `src/features/issues/queries.ts`

```ts
import { infiniteQueryOptions } from '@tanstack/react-query'

export const issuesInfiniteOptions = infiniteQueryOptions({
  queryKey: ['issues', 'infinite'] as const,
  initialPageParam: null as string | null,
  queryFn: async ({ pageParam, signal }) => {
    const search = pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''
    const response = await fetch(`/api/issues${search}`, { signal })
    if (!response.ok) throw new Error('Unable to load issues')
    return response.json() as Promise<{ items: Issue[]; nextCursor: string | null }>
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  maxPages: 5,
})
```

## Separate initial, next-page, and background states

A load-more action should check both hasNextPage and isFetchingNextPage to avoid duplicate requests. Distinguish initial pending, next-page loading, and background refetch states. maxPages bounds memory and sequential refetch cost; bidirectional lists must also define getPreviousPageParam.

## Practice and verification

### Completion checkpoint

Each load uses the cursor returned by the previous page, hasNextPage becomes false at the end, and pages always matches pageParams length.

### Exercise

Build a five-page-bounded issue timeline with load-more, then verify a filter change starts from the first page of a separate cache entry.

### Verification

Record three request cursors and simulate the final page, a retry, and rapid double-click; cursors do not repeat, the failed page retries, and duplicate next-page requests do not overlap.

### Common pitfalls

- Putting pageParam in the queryKey and splitting each page into its own query.
- Editing pages without preserving pageParams and corrupting InfiniteData.

## Official sources

- https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
- https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery
