# Infinite queries

> Paged queries with infiniteQueryOptions, "load more" buttons, infinite scroll, bidirectional lists and maxPages.

Lists that load more data onto what's already shown, such as "load more" buttons and infinite scroll, are a common UI pattern. A paged factory returns `infiniteQueryOptions(...)`, and components read it with `useInfiniteQuery`. Loaders and verbs take it like any query.

```ts title="src/queries.ts"
export function commentsQuery(input: { slug: string }) {
	return infiniteQueryOptions({
		queryKey: comments(input.slug),
		queryFn: (context) =>
			listComments({ slug: input.slug, cursor: context.pageParam }),
		initialPageParam: 0,
		getNextPageParam: (lastPage) => lastPage.nextCursor,
	});
}
```

- `comments` is a [group](https://raf-query.katt.dev/docs/query-keys.md#groups): it builds every comments key.
- `initialPageParam` is the first page's param.
- `getNextPageParam(lastPage, pages, lastPageParam)` returns the next page's param, or `null` / `undefined` when there are no more pages.
- The `queryFn` receives the param as `context.pageParam`, next to `signal`.

## Reading pages

`useInfiniteQuery(factory, handle, options?)` returns the same `[thenable, state]` tuple as [`useQuery`](https://raf-query.katt.dev/docs/reading-queries.md), with the same options. The data is `{ pages, pageParams }`, and the state adds paging:

```tsx
function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
	const [pagesThenable, comments] = useInfiniteQuery(
		commentsQuery,
		props.handle,
	);
	const data = use(pagesThenable);
	// const data: InfiniteData<CommentPage, number>

	return (
		<>
			{data.pages
				.flatMap((page) => page.items)
				.map((comment) => (
					<Comment
						key={comment.id}
						author={comment.author}
						body={comment.body}
					/>
				))}
			<button
				disabled={!comments.hasNextPage || comments.isFetchingNextPage}
				onClick={() => comments.fetchNextPage()}
			>
				Load more
			</button>
		</>
	);
}
```

| Control                                        | Meaning                                             |
| ---------------------------------------------- | --------------------------------------------------- |
| `fetchNextPage()`, `fetchPreviousPage()`       | Load a page at either end; resolves when it's there |
| `hasNextPage`, `hasPreviousPage`               | Whether the page param functions found one          |
| `isFetchingNextPage`, `isFetchingPreviousPage` | A page is loading at that end                       |

`useInfiniteQuery` takes only paged factories, and `useQuery` only factories that aren't: passing the other kind is a type error. The paging code is in `useInfiniteQuery` alone, so an app that never pages doesn't ship it.

## Infinite scroll

Infinite scroll is the same paged query with an automatic "Load more". A ref callback watches the button with an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) and presses it when it nears the viewport. Unmount the button while a page loads: it watches again when it remounts, so it keeps loading while it stays in reach. Its place is taken by skeletons the size of the incoming page, so the loaded rows replace them without moving anything below.

```tsx
{
	comments.isFetchingNextPage ? <CommentSkeletons /> : null;
}
{
	comments.hasNextPage && !comments.isFetchingNextPage ? (
		<button
			onClick={() => comments.fetchNextPage()}
			ref={(button) => {
				if (button === null) return;
				const observer = new IntersectionObserver(
					(entries) => {
						if (entries.some((entry) => entry.isIntersecting)) {
							observer.disconnect();
							void comments.fetchNextPage();
						}
					},
					{ rootMargin: "0px 0px 300px 0px" },
				);
				observer.observe(button);
				return () => observer.disconnect();
			}}
		>
			Load more
		</button>
	) : null;
}
```

Scroll the feed below. Switch the filter to see a new list start from its first page, and switch back to find the old one still cached, with every page you loaded.

> [!NOTE]
> The [web version of this page](https://raf-query.katt.dev/docs/infinite-queries) has an interactive demo here. Its code: [infinite-scroll.tsx](https://github.com/KATT/raf-query/blob/main/www/src/demos/infinite-scroll.tsx).

## Loading several pages up front

A loader's `swr` and `revalidate` load the first page. Pass `{ pages: n }` after the input to load more:

```ts
const [handles] = await load(queryClient, {
	comments: swr(commentsQuery, { slug }, { pages: 2 }),
});
```

The option only changes a first load: a query that already has pages keeps the ones it has.

## Refetching

When a paged query refetches (invalidated, refetched on focus, stale in a loader), every loaded page is refetched **in order**, starting from the first, so cursors stay consistent: page two's param is recomputed from the new page one. A refetch stops early if `getNextPageParam` finds no more pages.

Loading a page starts from the data as it was when the request started, and replaces the data when the page arrives.

## Bidirectional lists

Add `getPreviousPageParam(firstPage, pages, firstPageParam)` to load pages before the first one, for a chat log that starts in the middle:

```ts
infiniteQueryOptions({
	queryKey: messages(input.channel),
	queryFn: (context) =>
		listMessages({
			channel: input.channel,
			cursor: context.pageParam,
			signal: context.signal,
		}),
	initialPageParam: input.around,
	getNextPageParam: (lastPage) => lastPage.nextCursor,
	getPreviousPageParam: (firstPage) => firstPage.previousCursor,
});
```

`context.direction` tells the `queryFn` which way it's loading: `"backward"` for a page before the first, `"forward"` otherwise.

## Limiting pages with maxPages

Long sessions can accumulate many pages. `maxPages` keeps at most that many: when a new page is added, the page at the far end is dropped. Refetches then load fewer pages, too. With `maxPages`, define both `getNextPageParam` and `getPreviousPageParam`, so a dropped page can be loaded again when the user scrolls back.

## Changing the input

Pages belong to one input. A filter or sort in the URL changes the input, and with it the key, so a new list starts from its first page. The old list stays cached under its own key until `gcTime`, which is why switching back is instant. See [URL state and pagination](https://raf-query.katt.dev/docs/url-state-and-pagination.md).
