Infinite Async Chunk
Paginated async state with automatic infinite scroll support.
infiniteAsyncChunk() handles paginated data in accumulate mode — each page load appends to the existing list rather than replacing it. It's designed specifically for infinite scroll UIs.
import { infiniteAsyncChunk } from "stunk/query";
const postsChunk = infiniteAsyncChunk(
async ({ page, pageSize }) => {
const res = await fetch(`/api/posts?page=${page}&limit=${pageSize}`);
const json = await res.json();
return { data: json.posts, hasMore: json.hasMore };
},
{ pageSize: 20 },
);Options
| Option | Type | Default | Description |
|---|---|---|---|
pageSize | number | 10 | Items per page |
key | string | auto | Deduplication key — concurrent calls share one request |
enabled | boolean | () => boolean | true | Disable fetching until ready |
initialData | T[] | — | Seed data before the first fetch |
keepPreviousData | boolean | false | Show previous data while next page loads |
onSuccess | (data: T[]) => void | — | Called with the full accumulated array after each fetch |
onError | (error: E) => void | — | Called when all retries are exhausted |
retryCount | number | — | Retries on failure |
retryDelay | number | — | ms between retries |
staleTime | number | — | ms before data is considered stale |
cacheTime | number | — | ms to keep cache after last subscriber leaves |
refetchInterval | number | — | Auto-refetch interval in ms |
refetchOnWindowFocus | boolean | false | Refetch when window regains focus |
API reference
infiniteAsyncChunk inherits all asyncChunk APIs — reload(), refresh(), mutate(), setParams(), clearParams(), reset(), cleanup(), forceCleanup() — and adds the following:
nextPage()
Loads the next page and appends results to existing data. Stops when hasMore is false.
await postsChunk.nextPage();resetPagination()
Resets to page 1, clears accumulated data, and re-fetches.
await postsChunk.resetPagination();prevPage() and goToPage() are not available on infiniteAsyncChunk — infinite
scroll is forward-only. For bidirectional pagination use asyncChunk directly
with pagination: { mode: 'replace' }.
Fetcher shape
page and pageSize are injected automatically — never pass them in setParams:
async ({ page, pageSize, ...yourParams }) => {
return {
data: T[], // required — items for this page
hasMore?: boolean, // optional — whether more pages exist
total?: number, // optional — total item count
};
}In React
Attach observerTarget to a sentinel element at the bottom of your list — the hook handles intersection observation automatically:
import { infiniteAsyncChunk } from "stunk/query";
import { useInfiniteAsyncChunk } from "stunk/react";
const postsChunk = infiniteAsyncChunk(
async ({ page, pageSize }) => {
const res = await fetch(`/api/posts?page=${page}&limit=${pageSize}`);
const json = await res.json();
return { data: json.posts, hasMore: json.hasMore };
},
{ pageSize: 20 },
);
function PostFeed() {
const {
data,
loading,
error,
hasMore,
isFetchingMore,
loadMore,
observerTarget,
} = useInfiniteAsyncChunk(postsChunk);
if (loading && !data?.length) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<div ref={observerTarget} />
{isFetchingMore && <p>Loading more...</p>}
{!hasMore && <p>You've reached the end.</p>}
</div>
);
}useInfiniteAsyncChunk return values
| Value | Type | Description |
|---|---|---|
data | T[] | null | All accumulated items so far |
loading | boolean | true during any fetch |
error | E | null | Error from the latest failed fetch |
isPlaceholderData | boolean | true when showing stale data while next page loads |
hasMore | boolean | Whether more pages are available |
isFetchingMore | boolean | true when loading a subsequent page (data already exists) |
loadMore | () => void | Manually trigger the next page |
observerTarget | RefObject<HTMLElement> | Attach to a sentinel element for auto-loading |
reload | () => Promise<void> | Reload from page 1, clearing accumulated data |
refresh | () => Promise<void> | Refresh without resetting pagination |
Manual load more
Disable auto-scroll and trigger manually with autoLoad: false:
const { data, loadMore, hasMore, isFetchingMore } = useInfiniteAsyncChunk(
postsChunk,
{ autoLoad: false },
);
return (
<div>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
{hasMore && (
<button onClick={loadMore} disabled={isFetchingMore}>
{isFetchingMore ? "Loading..." : "Load more"}
</button>
)}
</div>
);With extra params
const postsChunk = infiniteAsyncChunk<Post, Error, { category: string }>(
async ({ page, pageSize, category }) => {
const res = await fetch(
`/api/posts?page=${page}&limit=${pageSize}&category=${category}`,
);
return { data: (await res.json()).posts, hasMore: true };
},
);
postsChunk.setParams({ category: "engineering" });page and pageSize are always injected automatically — never pass them in
setParams.