createInfiniteQuery
createInfiniteQuery creates a reusable infinite query object for paginated data. Perfect for infinite scroll, “load more” buttons, and cursor-based pagination.
Import
Section titled “Import”import { createInfiniteQuery, createInfiniteQueryFactory,} from "@ic-reactor/react"Basic Usage
Section titled “Basic Usage”import { createInfiniteQuery } from "@ic-reactor/react"import { backend } from "./reactor"
const postsQuery = createInfiniteQuery(backend, { functionName: "getPosts", initialPageParam: 0, getArgs: (offset) => [{ offset, limit: 20 }] as const, // Type inference getNextPageParam: (lastPage, allPages) => lastPage.posts.length < 20 ? undefined : allPages.length * 20,})Configuration
Section titled “Configuration”| Option | Type | Description |
|---|---|---|
functionName | string | The canister method to call (Required) |
initialPageParam | TPageParam | Initial cursor/offset value (Required) |
getArgs | (pageParam) => Args | Convert page param to method args (Required) |
getNextPageParam | function | Determine next page param (Required) |
getPreviousPageParam | function | For bi-directional scrolling |
maxPages | number | Max pages to keep in cache |
staleTime | number | Time before data is stale (ms) |
select | function | Transform the InfiniteData result |
callConfig | CallConfig | IC call configuration |
Pagination Functions
Section titled “Pagination Functions”// getNextPageParam signaturegetNextPageParam: ( lastPage: PageData, // Last fetched page allPages: PageData[], // All pages so far lastPageParam: Param, // Page param used for last fetch allPageParams: Param[] // All page params used) => Param | undefined | null
// getPreviousPageParam signature (for bi-directional)getPreviousPageParam: ( firstPage: PageData, allPages: PageData[], firstPageParam: Param, allPageParams: Param[]) => Param | undefined | nullReturn Value
Section titled “Return Value”| Property | Type | Description |
|---|---|---|
fetch | () => Promise<T> | Fetch first page (for loaders) |
useInfiniteQuery | (options?) => UseInfiniteQueryResult | React hook |
refetch | () => Promise<void> | Invalidate and refetch all pages |
getQueryKey | () => QueryKey | Get the query key |
getCacheData | (select?) => T | Read from cache |
Examples
Section titled “Examples”TanStack Router with Virtual Scrolling
Section titled “TanStack Router with Virtual Scrolling”Combine with TanStack Virtual for performant infinite scroll:
import { createFileRoute } from "@tanstack/react-router"import { useVirtualizer } from "@tanstack/react-virtual"import { createInfiniteQuery } from "@ic-reactor/react"import { backend } from "../reactor"import { useRef, useCallback } from "react"
const postsQuery = createInfiniteQuery(backend, { functionName: "getPosts", initialPageParam: 0, getArgs: (offset) => [{ offset, limit: 20 }] as const, getNextPageParam: (lastPage, allPages) => lastPage.hasMore ? allPages.length * 20 : undefined,})
export const Route = createFileRoute("/posts")({ // Prefetch first page loader: async () => { await postsQuery.fetch() }, component: PostsPage,})
function PostsPage() { const parentRef = useRef<HTMLDivElement>(null)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = postsQuery.useInfiniteQuery()
// Flatten all pages const allPosts = data?.pages.flatMap((page) => page.posts) ?? []
const virtualizer = useVirtualizer({ count: hasNextPage ? allPosts.length + 1 : allPosts.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, })
const items = virtualizer.getVirtualItems()
// Load more when reaching end const lastItem = items[items.length - 1] useEffect(() => { if ( lastItem?.index >= allPosts.length - 1 && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [lastItem?.index, hasNextPage, isFetchingNextPage, fetchNextPage])
return ( <div ref={parentRef} style={{ height: "100vh", overflow: "auto" }}> <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}> {items.map((virtualRow) => ( <div key={virtualRow.key} style={{ position: "absolute", top: virtualRow.start, width: "100%", }} > {virtualRow.index < allPosts.length ? ( <PostCard post={allPosts[virtualRow.index]} /> ) : ( <LoadingSpinner /> )} </div> ))} </div> </div> )}Cursor-Based Pagination
Section titled “Cursor-Based Pagination”Use cursors instead of offsets:
interface PostsResponse { posts: Post[] nextCursor: string | null}
const postsQuery = createInfiniteQuery(backend, { functionName: "getPosts", initialPageParam: null as string | null, getArgs: (cursor) => [{ cursor, limit: 20 }] as const, getNextPageParam: (lastPage: PostsResponse) => lastPage.nextCursor,})Bi-directional Scrolling
Section titled “Bi-directional Scrolling”Load pages in both directions (like a chat):
const messagesQuery = createInfiniteQuery(backend, { functionName: "getMessages", initialPageParam: { timestamp: Date.now() },
getArgs: (param) => [{ before: param.timestamp, limit: 50 }] as const,
// Load newer messages getNextPageParam: (lastPage) => lastPage.hasNewer ? { timestamp: lastPage.newestTimestamp } : undefined,
// Load older messages getPreviousPageParam: (firstPage) => firstPage.hasOlder ? { timestamp: firstPage.oldestTimestamp } : undefined,})
function ChatMessages() { const { data, fetchNextPage, fetchPreviousPage, hasPreviousPage, hasNextPage, isFetchingNextPage, isFetchingPreviousPage, } = messagesQuery.useInfiniteQuery()
return ( <div className="chat"> {hasPreviousPage && ( <button onClick={() => fetchPreviousPage()} disabled={isFetchingPreviousPage} > Load older messages </button> )}
{data?.pages.flatMap((page) => page.messages).map((msg) => ( <Message key={msg.id} message={msg} /> ))}
{hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage} > Load newer messages </button> )} </div> )}“Load More” Button Pattern
Section titled ““Load More” Button Pattern”Simple load more without infinite scroll:
function ProductList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = productsQuery.useInfiniteQuery()
const products = data?.pages.flatMap((page) => page.items) ?? []
return ( <div> <div className="product-grid"> {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div>
{hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage} className="load-more" > {isFetchingNextPage ? ( <><Spinner /> Loading...</> ) : ( `Load More (${products.length} items)` )} </button> )}
{!hasNextPage && products.length > 0 && ( <p className="end-message">You've reached the end!</p> )} </div> )}With Select Transformation
Section titled “With Select Transformation”Flatten pages automatically:
const postsQuery = createInfiniteQuery(backend, { functionName: "getPosts", initialPageParam: 0, getArgs: (offset) => [{ offset, limit: 20 }] as const, getNextPageParam: (lastPage) => lastPage.nextOffset,
// Flatten in the factory select: (data) => ({ posts: data.pages.flatMap((page) => page.posts), pageCount: data.pages.length, }),})
function PostList() { // data is already flattened const { data } = postsQuery.useInfiniteQuery()
return ( <div> <span>{data.pageCount} pages loaded</span> {data.posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> )}Intersection Observer for Auto-Load
Section titled “Intersection Observer for Auto-Load”Use IntersectionObserver for automatic loading:
function InfiniteList() { const loadMoreRef = useRef<HTMLDivElement>(null)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = postsQuery.useInfiniteQuery()
useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, { threshold: 0.1 } )
if (loadMoreRef.current) { observer.observe(loadMoreRef.current) }
return () => observer.disconnect() }, [hasNextPage, isFetchingNextPage, fetchNextPage])
return ( <div> {data?.pages.flatMap((page) => page.items).map((item) => ( <ItemCard key={item.id} item={item} /> ))}
<div ref={loadMoreRef} style={{ height: 20 }}> {isFetchingNextPage && <Spinner />} </div> </div> )}createInfiniteQueryFactory
Section titled “createInfiniteQueryFactory”For dynamic getArgs functions:
const getPostsQuery = createInfiniteQueryFactory(backend, { functionName: "getPosts", initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextOffset,})
// Create with specific args builderconst userPostsQuery = getPostsQuery((offset) => [ { userId, offset, limit: 20, },])
function UserPosts({ userId }: { userId: string }) { const { data } = userPostsQuery.useInfiniteQuery() // ...}See Also
Section titled “See Also”- createSuspenseInfiniteQuery — Suspense version
- useActorInfiniteQuery — Direct hook usage
- createQuery — Non-paginated queries