Skip to main content

Infinite Queries

Infinite queries are perfect for paginated data, infinite scroll, and "load more" patterns. They automatically manage page state and provide easy access to all loaded pages.

Basic Infinite Query​

const getUsers = client.infiniteQuery({
method: 'GET',
url: '/users',
querySchema: z.object({
cursor: z.string().optional(),
limit: z.number().optional(),
}),
responseSchema: z.object({
users: z.array(userSchema),
nextCursor: z.string().nullable(),
}),
processResponse: (data) => data,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: undefined,
})

Usage​

function UserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = getUsers.use({
params: { limit: 20 },
})

return (
<div>
{data?.pages.flatMap((page) =>
page.users.map((user) => (
<UserCard key={user.id} user={user} />
))
)}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}

Infinite Query from Endpoint​

// shared/endpoints/users.ts
export const getUsersEndpoint = API.declareEndpoint({
method: 'GET',
url: '/users',
querySchema: z.object({
cursor: z.string().optional(),
limit: z.number().optional(),
}),
responseSchema: z.object({
users: z.array(userSchema),
nextCursor: z.string().nullable(),
}),
})
// client/queries/users.ts
const getUsers = client.infiniteQueryFromEndpoint(getUsersEndpoint, {
processResponse: (data) => data,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: undefined,
})

Configuration Options​

getNextPageParam​

Extract the next page parameter from the last page:

const getUsers = client.infiniteQuery({
// ...
getNextPageParam: (lastPage, allPages) => {
// Return undefined to stop fetching
return lastPage.nextCursor ?? undefined
},
})

getPreviousPageParam​

For bidirectional pagination:

const getUsers = client.infiniteQuery({
// ...
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined,
initialPageParam: undefined,
})

initialPageParam​

Set the initial page parameter:

const getUsers = client.infiniteQuery({
// ...
initialPageParam: undefined, // or null, or a starting value
})

Pagination Patterns​

Cursor-Based​

const getUsers = client.infiniteQuery({
method: 'GET',
url: '/users',
querySchema: z.object({
cursor: z.string().optional(),
}),
responseSchema: z.object({
users: z.array(userSchema),
nextCursor: z.string().nullable(),
}),
processResponse: (data) => data,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: undefined,
})

Offset-Based​

const getUsers = client.infiniteQuery({
method: 'GET',
url: '/users',
querySchema: z.object({
offset: z.number().optional(),
limit: z.number().optional(),
}),
responseSchema: z.object({
users: z.array(userSchema),
hasMore: z.boolean(),
}),
processResponse: (data) => data,
getNextPageParam: (lastPage, allPages) => {
if (!lastPage.hasMore) return undefined
const currentOffset = allPages.length * 20
return currentOffset
},
initialPageParam: 0,
})

Page Number​

const getUsers = client.infiniteQuery({
method: 'GET',
url: '/users',
querySchema: z.object({
page: z.number().optional(),
}),
responseSchema: z.object({
users: z.array(userSchema),
totalPages: z.number(),
}),
processResponse: (data) => data,
getNextPageParam: (lastPage, allPages) => {
const nextPage = allPages.length + 1
return nextPage <= lastPage.totalPages ? nextPage : undefined
},
initialPageParam: 1,
})

Accessing Data​

All Pages​

const { data } = getUsers.use({ params: { limit: 20 } })

// data.pages is an array of all loaded pages
data?.pages.forEach((page, index) => {
console.log(`Page ${index + 1}:`, page.users)
})

Flattened Data​

const { data } = getUsers.use({ params: { limit: 20 } })

// Flatten all pages into a single array
const allUsers = data?.pages.flatMap((page) => page.users) ?? []

Current Page​

const { data } = getUsers.use({ params: { limit: 20 } })

// Get the last page
const lastPage = data?.pages[data.pages.length - 1]

Loading States​

const {
data,
isLoading, // Initial load
isFetching, // Any fetch
isFetchingNextPage, // Fetching next page
isRefetching, // Refetching all pages
} = getUsers.use({ params: { limit: 20 } })

Common Patterns​

Infinite Scroll​

function InfiniteUserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = getUsers.use({ params: { limit: 20 } })

useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >=
document.documentElement.offsetHeight - 100
) {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
}
}

window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [hasNextPage, isFetchingNextPage, fetchNextPage])

return (
<div>
{data?.pages.flatMap((page) =>
page.users.map((user) => (
<UserCard key={user.id} user={user} />
))
)}
{isFetchingNextPage && <div>Loading more...</div>}
</div>
)
}

Load More Button​

function UserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = getUsers.use({ params: { limit: 20 } })

return (
<div>
{data?.pages.flatMap((page) =>
page.users.map((user) => (
<UserCard key={user.id} user={user} />
))
)}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}

With Suspense​

function InfiniteUserList() {
const data = getUsers.useSuspense({ params: { limit: 20 } })

return (
<div>
{data.pages.flatMap((page) =>
page.users.map((user) => (
<UserCard key={user.id} user={user} />
))
)}
</div>
)
}

Refetching​

Refetch All Pages​

const { refetch } = getUsers.use({ params: { limit: 20 } })

// Refetches all pages
<button onClick={() => refetch()}>Refresh All</button>

Refetch from Specific Page​

const { refetch } = getUsers.use({ params: { limit: 20 } })

// Refetch from page 2 onwards
refetch({ refetchPage: (page, index) => index >= 1 })

Next Steps​