URL-Based Pagination
Handle pagination state in URLs with TanStack Router
How URL Pagination Works
Store pagination state in URL search parameters for shareable, bookmarkable, and refreshable pagination. TanStack Router's search param system provides type-safe pagination state management.
Basic Pagination Setup
Route Configuration
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'
const paginationSchema = z.object({
page: z.number().default(1),
limit: z.number().default(20),
})
export const Route = createFileRoute('/posts')({
validateSearch: zodValidator(paginationSchema.optional()),
loaderDeps: ({ search }) => ({ search }),
loader: ({ deps: { search } }) => {
return fetchPosts({
page: search.page,
limit: search.limit,
})
},
component: PostsList,
})Basic Pagination Component
function PostsList() {
const { page, limit } = Route.useSearch()
const { posts, totalPages } = Route.useLoaderData()
const navigate = useNavigate({ from: '/posts' })
const goToPage = (newPage: number) => {
navigate({
to: ".",
search: (prev) => ({ ...prev, page: newPage }),
})
}
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
<div className="pagination">
<button
disabled={page === 1}
onClick={() => goToPage(page - 1)}
>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
disabled={page === totalPages}
onClick={() => goToPage(page + 1)}
>
Next
</button>
</div>
</div>
)
}Infinite Scroll with URL State
const infiniteSchema = z.object({
limit: z.number().default(20),
cursor: z.string().optional(),
})
function InfinitePostsList() {
const search = Route.useSearch()
const navigate = useNavigate({ from: '/posts' })
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts', search],
queryFn: ({ pageParam }) => fetchPosts({
limit: search.limit,
cursor: pageParam,
}),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Update URL when loading more
const loadMore = () => {
if (hasNextPage) {
const nextCursor = data?.pages[data.pages.length - 1]?.nextCursor
if (nextCursor) {
navigate({
search: (prev) => ({ ...prev, cursor: nextCursor }),
})
}
fetchNextPage()
}
}
return (
<div>
{data?.pages.map(page =>
page.posts.map(post => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={loadMore} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}