Skip to content
IC Reactor

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 { 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>
)
}

UseActorInfiniteQueryParameters extends react-query’s UseInfiniteQueryOptions, giving you access to all standard react-query options plus our custom actor 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
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.

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 | null
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
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
}