Infinite queries are perfect for implementing infinite scrolling, “load more” buttons, and other patterns where you need to fetch data in pages or chunks.
Basic Usage
Use useInfiniteQuery (React) or createInfiniteQuery (Solid/Vue) to implement infinite data loading:
import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const {
data,
error,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetching,
isFetchingNextPage,
isFetchingPreviousPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/projects?cursor=${pageParam}`)
return await response.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
})
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
)
}
Required Options
initialPageParam
The initial page param to use when fetching the first page:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0, // Required!
})
getNextPageParam
Function that receives the last page and all pages/pageParams, and returns the next page param:
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
// Return undefined when there's no next page
return lastPage.nextCursor ?? undefined
}
Return undefined when there are no more pages to indicate the end of data.
getPreviousPageParam (Optional)
Function to determine the previous page param for bi-directional infinite queries:
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
return firstPage.prevCursor ?? undefined
}
Data Structure
The data object has a specific structure for infinite queries:
interface InfiniteData<TData, TPageParam> {
pages: TData[] // Array of pages
pageParams: TPageParam[] // Array of page params used to fetch each page
}
Example:
{
pages: [
{ data: [...], nextCursor: 2 },
{ data: [...], nextCursor: 3 },
{ data: [...], nextCursor: 4 },
],
pageParams: [0, 2, 3]
}
Fetching Pages
fetchNextPage
Fetch the next page of data:
const { fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
// ... options
})
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
Load More
</button>
fetchPreviousPage
Fetch the previous page (for bi-directional scrolling):
const { fetchPreviousPage, hasPreviousPage, isFetchingPreviousPage } = useInfiniteQuery({
// ... options
})
<button
onClick={() => fetchPreviousPage()}
disabled={!hasPreviousPage || isFetchingPreviousPage}
>
Load Older
</button>
Limiting Pages with maxPages
Limit the number of pages stored in memory to optimize performance:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
maxPages: 3, // Only keep 3 pages in memory
})
When maxPages is set, older pages will be removed from the cache as new pages are fetched. When fetching in the forward direction, the oldest pages are removed first. When fetching backward, the newest pages are removed.
Refetching Pages
By default, refetching an infinite query will refetch all pages. You can customize this:
// Refetch only the first page
queryClient.refetchQueries({
queryKey: ['projects'],
refetchPage: (page, index) => index === 0,
})
Bi-directional Infinite Lists
Example of a chat-like interface with both directions:
function Chat() {
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
} = useInfiniteQuery({
queryKey: ['messages'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/messages?cursor=${pageParam}`)
return res.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
})
return (
<div>
<button onClick={() => fetchPreviousPage()} disabled={!hasPreviousPage}>
Load Older Messages
</button>
{data?.pages.map((page) => (
page.messages.map((message) => (
<Message key={message.id} {...message} />
))
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load Newer Messages
</button>
</div>
)
}
Status Flags
Infinite queries provide specialized status flags:
isFetchingNextPage: true while fetching the next page
isFetchingPreviousPage: true while fetching the previous page
isFetchNextPageError: true if fetching next page failed
isFetchPreviousPageError: true if fetching previous page failed
hasNextPage: true if there is a next page to fetch
hasPreviousPage: true if there is a previous page to fetch
Cursor-based (Recommended)
useInfiniteQuery({
queryKey: ['items'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/items?cursor=${pageParam}`)
return res.json()
},
initialPageParam: null,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
Offset-based
useInfiniteQuery({
queryKey: ['items'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/items?offset=${pageParam}&limit=10`)
return res.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length * 10 : undefined
},
})
Cursor-based pagination is generally more reliable for real-time data since it’s not affected by insertions/deletions in the dataset.
Flattening Pages
If you need a flat array of items instead of pages:
const { data } = useInfiniteQuery({
queryKey: ['items'],
queryFn: fetchItems,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
select: (data) => ({
pages: [...data.pages],
pageParams: [...data.pageParams],
// Flatten the pages into a single array
flatPages: data.pages.flatMap((page) => page.items),
}),
})
// Use the flattened data
data?.flatPages.map((item) => <Item key={item.id} {...item} />)
Next Steps