Guides
Infinite Queries
Implement pagination with cursor-based infinite queries
Infinite Queries
Use infiniteQueryOptions() for cursor-based pagination with TanStack Query's useInfiniteQuery.
Server Route
A typical cursor-based pagination endpoint:
app.get('/posts', ({ query }) => {
const { limit, cursor } = query
const posts = getPosts({ limit, after: cursor })
return {
items: posts,
nextCursor: posts.length === limit ? posts[posts.length - 1].id : null,
}
}, {
query: t.Object({
limit: t.Number({ default: 10 }),
cursor: t.Optional(t.String()),
})
})Basic Usage
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEden } from './lib/eden'
function PostList() {
const eden = useEden()
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery(
eden.posts.get.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialCursor: null,
}
)
)
if (isLoading) return <div>Loading...</div>
return (
<div>
{data?.pages.map((page) =>
page.items.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'No more posts'}
</button>
</div>
)
}Required Options
Two options are required when using infiniteQueryOptions():
getNextPageParam -- determines the cursor for the next page. Return undefined to indicate there are no more pages:
{
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined
}initialCursor -- the cursor value for the first page:
{
initialCursor: null // or 0, '', depending on your API
}Input Parameters
The first argument is your query input (excluding cursor). The library automatically adds the cursor to each page request:
eden.posts.get.infiniteQueryOptions(
{ limit: 10, category: 'technology' },
{
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialCursor: null,
}
)Path Parameters
Combine path parameters with pagination:
// GET /users/:id/posts
function UserPosts({ userId }: { userId: string }) {
const eden = useEden()
const { data } = useInfiniteQuery(
eden.users({ id: userId }).posts.get.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (page) => page.nextCursor ?? undefined,
initialCursor: null,
}
)
)
}Infinite Query Key and Filter
Use infiniteQueryKey() and infiniteQueryFilter() for cache operations:
const queryClient = useQueryClient()
const eden = useEden()
// Get the infinite query key
const key = eden.posts.get.infiniteQueryKey({ limit: 10 })
// Invalidate infinite query
queryClient.invalidateQueries({
queryKey: eden.posts.get.infiniteQueryKey()
})
// Using infiniteQueryFilter
queryClient.invalidateQueries(
eden.posts.get.infiniteQueryFilter({ category: 'tech' })
)Conditional with skipToken
import { skipToken } from '@tanstack/react-query'
function UserPosts({ userId }: { userId: string | null }) {
const eden = useEden()
const { data } = useInfiniteQuery(
eden.users({ id: userId ?? '' }).posts.get.infiniteQueryOptions(
userId ? { limit: 10 } : skipToken,
{
getNextPageParam: (page) => page.nextCursor ?? undefined,
initialCursor: null,
}
)
)
}Abort on Unmount
const { data } = useInfiniteQuery(
eden.posts.get.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (page) => page.nextCursor ?? undefined,
initialCursor: null,
eden: { abortOnUnmount: true },
}
)
)