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