useActorInfiniteQuery
useActorInfiniteQuery is a React hook for fetching paginated data from Internet Computer canisters. It wraps TanStack Query’s useInfiniteQuery with canister-specific functionality.
Import
Section titled “Import”import { createActorHooks } from "@ic-reactor/react"import { backend } from "./reactor"
const { useActorInfiniteQuery } = createActorHooks(backend)import { useActorInfiniteQuery } from "./reactor"
function ItemList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useActorInfiniteQuery({ functionName: "get_items", getArgs: (pageParam) => [{ offset: pageParam, limit: 10 }] as const, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextOffset ?? undefined, })
return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.items.map((item) => ( <Item key={item.id} data={item} /> ))} </div> ))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? "Loading more..." : hasNextPage ? "Load More" : "No more items"} </button> </div> )}Options
Section titled “Options”UseActorInfiniteQueryParameters extends react-query’s UseInfiniteQueryOptions, giving you access to all standard react-query options plus our custom actor options.
Required Options
Section titled “Required Options”| Option | Type | Description |
|---|---|---|
functionName |
string |
The canister method to call |
getArgs |
((pageParam) => Args) | skipToken |
Function that returns args for each page, or skipToken while they are not known |
initialPageParam |
TPageParam |
Initial page parameter |
getNextPageParam |
function |
Returns next page param or undefined |
Optional Options
Section titled “Optional Options”| Option | Type | Default | Description |
|---|---|---|---|
getPreviousPageParam |
function |
- | Returns previous page param for bi-directional |
maxPages |
number |
- | Maximum number of pages to keep in cache |
enabled |
boolean |
true |
Whether the query should run |
staleTime |
number |
0 |
Time in ms before data is stale |
gcTime |
number |
300000 |
Time before unused data is GC’d |
refetchOnWindowFocus |
boolean |
true |
Refetch on window focus |
select |
function |
- | Transform the data |
queryKey |
array |
- | Additional query key segments |
getKeyArgs |
function |
- | Narrows the args that key the cache (see below) |
The cache key includes the first page’s arguments, getArgs(initialPageParam),
so two lists over the same method with different arguments stay apart. When
those arguments carry the cursor, getKeyArgs returns the part that identifies
the list. It is required when initialPageParam changes between renders, as
Date.now() does: without it every render keys a new query and refetches the
first page.
While the list’s arguments are not known yet, pass TanStack Query’s
skipToken (re-exported by @ic-reactor/react) as getArgs. The list waits
without calling the canister, in a cache entry of its own under its method and
queryKey, and fetches its first page once getArgs is a function:
import { skipToken } from "@ic-reactor/react"import { useActorInfiniteQuery } from "./reactor"
function AuthorPosts({ authorId }: { authorId: string | undefined }) { const { data } = useActorInfiniteQuery({ functionName: "list_posts", getArgs: authorId ? (offset: number) => [{ authorId, offset, limit: 10 }] : skipToken, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextOffset, })
return ( <PostList posts={data?.pages.flatMap((page) => page.posts)} author={authorId} /> )}useActorSuspenseInfiniteQuery does not take skipToken.
Callback Signatures
Section titled “Callback Signatures”getArgs: (pageParam: TPageParam) => Args
getNextPageParam: ( lastPage: TData, allPages: TData[], lastPageParam: TPageParam, allPageParams: TPageParam[]) => TPageParam | undefined | null
getPreviousPageParam: ( firstPage: TData, allPages: TData[], firstPageParam: TPageParam, allPageParams: TPageParam[]) => TPageParam | undefined | nullReturn Value
Section titled “Return Value”| Property | Type | Description |
|---|---|---|
data |
InfiniteData<TData> |
Object with pages and pageParams arrays |
data.pages |
TData[] |
Array of all fetched pages |
data.pageParams |
TPageParam[] |
Array of page parameters used |
error |
Error | null |
Error if query failed |
status |
'pending' | 'error' | 'success' |
Query status |
isPending |
boolean |
True if no data yet |
isFetching |
boolean |
True if any fetch in progress |
isFetchingNextPage |
boolean |
True if fetching next page |
isFetchingPreviousPage |
boolean |
True if fetching previous page |
isRefetching |
boolean |
True if refetching all pages |
hasNextPage |
boolean |
True if more pages available |
hasPreviousPage |
boolean |
True if previous pages available |
fetchNextPage |
function |
Fetch the next page |
fetchPreviousPage |
function |
Fetch the previous page |
refetch |
function |
Refetch all pages |
Examples
Section titled “Examples”Offset-Based Pagination
Section titled “Offset-Based Pagination”import { useActorInfiniteQuery } from "./reactor"
function Items() { const { data, fetchNextPage, hasNextPage } = useActorInfiniteQuery({ functionName: "get_items", getArgs: (offset) => [{ offset, limit: 20 }], initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { // If we got fewer items than limit, no more pages return lastPage.items.length < 20 ? undefined : allPages.length * 20 // Next offset }, })
return ( <div> {data?.pages.map((page, i) => ( <ul key={i}> {page.items.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </ul> ))} {hasNextPage && <button onClick={() => fetchNextPage()}>More</button>} </div> )}Cursor-Based Pagination
Section titled “Cursor-Based Pagination”import { useActorInfiniteQuery } from "./reactor"
function Items() { const { data, fetchNextPage, hasNextPage } = useActorInfiniteQuery({ functionName: "get_items_after", getArgs: (cursor) => [cursor ? { after: cursor } : {}], initialPageParam: null as string | null, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, })
return ( <div> {data?.pages.map((page, i) => ( <ul key={i}> {page.items.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </ul> ))} {hasNextPage && <button onClick={() => fetchNextPage()}>More</button>} </div> )}Page Number Pagination
Section titled “Page Number Pagination”import { useActorInfiniteQuery } from "./reactor"
function Items() { const { data, fetchNextPage, hasNextPage } = useActorInfiniteQuery({ functionName: "get_items_by_page", getArgs: (page) => [{ page, perPage: 10 }], initialPageParam: 1, getNextPageParam: (lastPage, allPages, lastPageParam) => { return lastPage.hasMore ? lastPageParam + 1 : undefined }, })
return ( <div> {data?.pages.map((page, i) => ( <ul key={i}> {page.items.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </ul> ))} {hasNextPage && <button onClick={() => fetchNextPage()}>More</button>} </div> )}Bi-Directional Infinite List
Section titled “Bi-Directional Infinite List”import { useActorInfiniteQuery } from "./reactor"
function Timeline() { const { data, fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, } = useActorInfiniteQuery({ functionName: "get_timeline", getArgs: (timestamp) => [{ around: timestamp, limit: 20 }], // A nat64 is text in a DisplayReactor initialPageParam: String(Date.now()), // `Date.now()` differs on every render, so keep the cursor out of the key. getKeyArgs: ([{ limit }]) => [{ limit }], getNextPageParam: (lastPage) => lastPage.oldestTimestamp, getPreviousPageParam: (firstPage) => firstPage.newestTimestamp, })
return ( <div> <button onClick={() => fetchPreviousPage()} disabled={!hasPreviousPage}> Load newer </button>
{data?.pages.map((page, i) => ( <TimelineItems key={i} items={page.items} /> ))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}> Load older </button> </div> )}Infinite Scroll
Section titled “Infinite Scroll”import { useInView } from "react-intersection-observer"import { useEffect } from "react"import { useActorInfiniteQuery } from "./reactor"
function InfiniteList() { const { ref, inView } = useInView()
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useActorInfiniteQuery({ functionName: "get_items", getArgs: (offset) => [{ offset, limit: 20 }], initialPageParam: 0, getNextPageParam: (lastPage, allPages) => lastPage.items.length < 20 ? undefined : allPages.length * 20, })
// Auto-fetch when sentinel comes into view useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage])
return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.items.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </div> ))}
{/* Sentinel element */} <div ref={ref}>{isFetchingNextPage && <LoadingSpinner />}</div> </div> )}With Select for Flattened Data
Section titled “With Select for Flattened Data”import { useActorInfiniteQuery } from "./reactor"
function Items() { const { data: items } = useActorInfiniteQuery({ functionName: "get_items", getArgs: (offset) => [{ offset, limit: 20 }], initialPageParam: 0, getNextPageParam: (lastPage, allPages) => lastPage.items.length < 20 ? undefined : allPages.length * 20, // Flatten all pages into a single array select: (data) => ({ pages: data.pages.flatMap((page) => page.items), pageParams: data.pageParams, }), })
// items.pages is now a flat array of all items return ( <ul> {items?.pages.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </ul> )}Limiting Cached Pages
Section titled “Limiting Cached Pages”import { useActorInfiniteQuery } from "./reactor"
function RecentItems() { const { data } = useActorInfiniteQuery({ functionName: "get_items", getArgs: (offset) => [{ offset, limit: 20 }], initialPageParam: 0, getNextPageParam: (lastPage, allPages) => lastPage.items.length < 20 ? undefined : allPages.length * 20, maxPages: 3, // Only keep last 3 pages in cache })
return <span>{data?.pages.length} pages cached</span>}See Also
Section titled “See Also”- useActorSuspenseInfiniteQuery — Suspense version
- useActorQuery — Regular queries
- Queries Guide — In-depth query patterns
- createActorHooks — Creating hooks