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
Open in StackBlitzEdit and run in your browser
View on GitHubBrowse the source code
Live Preview
Section titled “Live Preview”Key Code
Section titled “Key Code”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], }) // ...}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, })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> )}