Infinite Query
Shows how to implement infinite scrolling with IC Reactor using TanStack Query’s useInfiniteQuery.
Features Demonstrated
Section titled “Features Demonstrated”useActorInfiniteQuery/useActorSuspenseInfiniteQueryfor paginated datacreateSuspenseInfiniteQueryFactoryfor a reusable, arg-parameterised query- Cursor-based pagination
- Load more on scroll with an
IntersectionObserversentinel getArgs+initialPageParam+getNextPageParamfor automatic pagination
Live Preview
Section titled “Live Preview”Key Code
Section titled “Key Code”The canister method is get_posts : (opt text, nat, nat) -> (PostsResponse),
where PostsResponse is { posts : vec Post; next_cursor : opt nat }. 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): [[] | [string], bigint, bigint] => [[], cursor, 10n], getNextPageParam: (lastPage) => lastPage.next_cursor.length === 0 ? undefined : lastPage.next_cursor[0], }) // ...}Here getArgs spells out its return type. useReactorInfiniteQuery receives
the reactor in the same call, so TypeScript types the returned array as
(bigint | never[])[] rather than the argument tuple. as const doesn’t help
for this method: it also makes the empty opt array readonly, which
[] | [string] rejects.
With Suspense
Section titled “With Suspense”The demo defines the query once at module scope with
createSuspenseInfiniteQueryFactory, so getArgs is supplied per call site
instead of being baked in:
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, })import { getPostsQuery } from "./store"
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> )}