# URL state and pagination

> Put filters, sort and page in the URL, let the loader turn them into the query's input, and pick what shows while the new input loads with whilePending.

Filters, sort order and pagination are search params. The loader turns them into the query's input, so every navigation starts the right query and the component only ever reads the handle it was given:

```tsx title="src/routes/posts.tsx"
import { z } from "zod";

const PostsSearch = z.object({
	sort: z.enum(["newest", "oldest"]).default("newest"),
	page: z.int().positive().default(1),
});

export const Route = createFileRoute("/posts")({
	validateSearch: PostsSearch,
	loaderDeps: (args) => args.search,
	loader: async (args) => {
		const [handles] = await load(args.context.queryClient, {
			posts: prefetch(postsQuery, args.deps),
		});
		return handles;
	},
	component: PostsPage,
});

function PostsPage() {
	const loaderData = Route.useLoaderData();
	const [postsThenable, postsState] = useQuery(postsQuery, loaderData.posts, {
		whilePending: "previous",
	});
	const posts = use(postsThenable);

	return (
		<>
			<nav aria-busy={postsState.isPrevious}>
				<Link to="." search={(prev) => ({ ...prev, sort: "newest" })}>
					Newest
				</Link>
				<Link to="." search={(prev) => ({ ...prev, sort: "oldest" })}>
					Oldest
				</Link>
			</nav>
			<PostList posts={posts} dimmed={postsState.isPrevious} />
		</>
	);
}
```

Clicking a link navigates, the loader starts `{ sort: "oldest", page: 1 }` with `prefetch` without blocking, and the component receives a new handle.

## What shows while the new input loads

That's up to `useQuery`'s `whilePending` option:

| `whilePending`        | While the new input loads                                                                                                                                                                  | Use for                                                       |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `"suspend"` (default) | `use()` suspends. TanStack Router and Next.js run navigations as React transitions, so React keeps the whole previous screen, including the old URL's active links, until the data arrives | Short loads, and pages that shouldn't show stale data         |
| `"previous"`          | The navigation commits immediately. `useQuery` keeps returning the previous input's data, and `isPrevious` is `true` on its state until the new data lands                                 | Lists, filters, pagination: anything that should feel instant |

Either way, an input that's already cached renders immediately. A component that mounts with a pending handle has no previous data, so `"previous"` suspends like `"suspend"` on first load.

Page through the demo with each mode. Watch the address bar: with `"suspend"` it waits for the data, with `"previous"` it updates at once and the old page dims. Hovering **Next** prefetches the next page, so with a bit of patience neither mode waits.

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

`"previous"` covers what TanStack Query's [`placeholderData: keepPreviousData`](https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata) and `isPlaceholderData` do.

## Skeletons for unrelated content

To show a skeleton instead, for instance when moving between unrelated posts, give the Suspense boundary the query's key. React treats it as new content and shows its fallback, even inside a transition:

```tsx
// This loader starts the post with `prefetch`.
const [postThenable, postState] = useQuery(postQuery, loaderData.post);

return (
	<Suspense key={postState.hash} fallback={<PostSkeleton />}>
		<PostBody post={postThenable} />
	</Suspense>
);
```

`hash` on the state is the stable string form of the query's key.

## Input typed before it reaches the URL

A search box changes the input on every keystroke, faster than navigations should run. Keep what's being typed in state, and derive the reader's handle from the loader's with `waterfallHandle(factory, handle, update)`. `update` gets the handle's input and returns the new one:

```tsx
// `props.search` is the handle the loader started from the URL's `?q=`.
function Search(props: { search: QueryHandle<typeof searchQuery> }) {
	const [q, setQ] = useState(props.search.input.q);
	const searchHandle = waterfallHandle(searchQuery, props.search, (input) => ({
		...input,
		q,
	}));
	const [resultsThenable, resultsState] = useQuery(searchQuery, searchHandle, {
		whilePending: "previous",
	});
	const results = use(resultsThenable);

	return (
		<>
			<input value={q} onChange={(event) => setQ(event.target.value)} />
			<Results posts={results} dimmed={resultsState.isPrevious} />
		</>
	);
}
```

While `q` is still the URL's, the new input maps to the same query, so `waterfallHandle` returns the loader's handle itself and `useQuery` reads it as before. Each new `q` is a query no loader started: the component fetches it while rendering, a [request waterfall](https://raf-query.katt.dev/docs/request-waterfalls.md#render-time-inputs-waterfallhandle) the name keeps visible. Write the term to the URL when the user submits or pauses, so the next loader starts it and links share it.

## Pagination versus infinite scroll

They differ in where the page lives:

- **Pagination**: the page is a search param and part of the input, so each page is its own query and the URL links to it.
- **Infinite scroll**: filters and sort are search params, and pages are loaded into one paged query per input with `infiniteQueryOptions`, read with `useInfiniteQuery` and extended with `fetchNextPage`. Changing a filter starts a new list from its first page. See [Infinite queries](https://raf-query.katt.dev/docs/infinite-queries.md).

## Why not local state?

It's tempting to keep the page number in `useState` and build the input from it. Don't: the loader can't see component state, so the query would start only after render, the back button wouldn't restore the page, and a shared link would open page one. Keeping the input in the URL makes every view linkable and every query startable before render.

In Next.js the same rules apply: read [`searchParams`](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) in the page, start the query with them in `load`, and pass the handle to the client component.
