Skip to content
IC Reactor

Infinite Query

Shows how to implement infinite scrolling with IC Reactor using TanStack Query’s useInfiniteQuery.

  • useActorInfiniteQuery / useActorSuspenseInfiniteQuery for paginated data
  • createSuspenseInfiniteQueryFactory for a reusable, arg-parameterised query
  • Cursor-based pagination
  • Load more on scroll with an IntersectionObserver sentinel
  • getArgs + initialPageParam + getNextPageParam for automatic pagination

The canister method is get_posts : (opt text, nat64, nat64) -> (PostsResponse), where PostsResponse is { posts : vec Post; next_cursor : opt nat64 }. Three options are required on every infinite query: getArgs turns the page parameter into the call arguments, initialPageParam seeds the first page, and getNextPageParam reads the cursor off the last page.

import { useBackendInfiniteQuery } from "./declarations/backend"
function InfiniteList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useBackendInfiniteQuery({
functionName: "get_posts",
initialPageParam: 0n,
getArgs: (cursor) => [[], cursor, 10n],
getNextPageParam: (lastPage) =>
lastPage.next_cursor.length === 0 ? undefined : lastPage.next_cursor[0],
})
return (
<div>
{data?.pages.map((page) =>
page.posts.map((post) => (
<PostCard key={post.id.toString()} post={post} />
))
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
</div>
)
}

Without generated hooks, the same query reads:

import { useReactorInfiniteQuery } from "@ic-reactor/react"
import { backendReactor } from "./declarations/backend"
function InfiniteList() {
const { data } = useReactorInfiniteQuery({
reactor: backendReactor,
functionName: "get_posts",
initialPageParam: 0n,
getArgs: (cursor) => [[], cursor, 10n],
getNextPageParam: (lastPage) =>
lastPage.next_cursor.length === 0 ? undefined : lastPage.next_cursor[0],
})
// ...
}

The demo defines the query once at module scope with createSuspenseInfiniteQueryFactory, so getArgs is supplied per call site instead of being baked in:

src/store.ts
import { createSuspenseInfiniteQueryFactory } from "@ic-reactor/react"
import { backendReactor } from "./declarations/backend"
export const getPostsQuery = createSuspenseInfiniteQueryFactory(
backendReactor,
{
functionName: "get_posts",
initialPageParam: 0n,
getNextPageParam: (lastPage) => {
if (lastPage.next_cursor.length === 0) return undefined
return lastPage.next_cursor[0]
},
gcTime: 1000 * 60 * 5,
staleTime: 1000 * 60 * 1,
}
)
src/App.tsx
function Feed({ category }: { category: string }) {
const postsQuery = getPostsQuery(
(cursor) => [category === "All" ? [] : [category], cursor, 10n],
{ queryKey: [category] }
)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
postsQuery.useSuspenseInfiniteQuery()
const allPosts = data.pages.flatMap((p) => p.posts)
// No need for a loading state — Suspense handles it
return (
<div>
{allPosts.map((post) => (
<PostCard key={post.id.toString()} post={post} />
))}
</div>
)
}