StunkStunk
Async

Infinite Async Chunk

Paginated async state with automatic infinite scroll support.

infiniteAsyncChunk() is built on top of asyncChunk() and 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";

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,
      total: json.total,
    };
  },
  { pageSize: 20 },
);

API reference

infiniteAsyncChunk inherits all asyncChunk APIs — reload(), refresh(), mutate(), setParams(), reset(), cleanup() — and adds the following:

nextPage()

Loads the next page and appends results to the existing data.

await postsChunk.nextPage();

Stops automatically when hasMore is false.

resetPagination()

Resets back to page 1 and clears accumulated data, then re-fetches.

await postsChunk.resetPagination();

prevPage() and goToPage() are not available on infiniteAsyncChunk — infinite scroll is forward-only. If you need bidirectional pagination, use asyncChunk directly with pagination: { mode: 'replace' }.


On each nextPage() call, the fetcher receives an incremented page number alongside your pageSize. Results are accumulated — new items are appended to data rather than replacing it. Pagination stops automatically when hasMore is false.


Fetcher shape

The fetcher always receives page and pageSize automatically — you don't need to manage them:

async ({ page, pageSize, ...yourParams }) => {
  // fetch page `page` of size `pageSize`
  return {
    data: T[],       // required — items for this page
    hasMore?: boolean, // optional — whether more pages exist
    total?: number,    // optional — total item count
  };
}

Options

OptionTypeDefaultDescription
pageSizenumber10Number of items per page
staleTimenumberms before data is considered stale
cacheTimenumberms to keep cache after last subscriber
retryCountnumberNumber of retries on failure
retryDelaynumberms between retries
onError(error) => voidError callback

In React

Use useInfiniteAsyncChunk to wire up infinite scroll with zero manual observer setup. Attach the returned observerTarget ref to a sentinel element at the bottom of your list — the hook handles the rest automatically.

import { infiniteAsyncChunk } from "stunk";
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>

      {/* Sentinel — triggers next page automatically when visible */}
      <div ref={observerTarget} />

      {isFetchingMore && <p>Loading more...</p>}
      {!hasMore && <p>You've reached the end.</p>}
    </div>
  );
}

useInfiniteAsyncChunk return values

ValueTypeDescription
dataT[] | nullAll accumulated items so far
loadingbooleantrue during any fetch
errorE | nullError from the latest failed fetch
hasMorebooleanWhether more pages are available
isFetchingMorebooleantrue when loading a subsequent page (data already exists)
loadMore() => voidManually trigger the next page
observerTargetRefObject<HTMLDivElement>Attach to a sentinel element for auto-loading
reload() => Promise<void>Reload from page 1, clearing accumulated data
refresh() => Promise<void>Refresh current data without resetting pagination

Manual pagination

If you don't want auto-scroll behavior, set autoLoad: false and use loadMore yourself:

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

You can pass custom params alongside the auto-managed page and pageSize:

const postsChunk = infiniteAsyncChunk<Post, Error, { category: string }>(
  async ({ page, pageSize, category }) => {
    const res = await fetch(
      `/api/posts?page=${page}&limit=${pageSize}&category=${category}`,
    );
    const json = await res.json();
    return { data: json.posts, hasMore: json.hasMore };
  },
);

postsChunk.setParams({ category: "engineering" });

page and pageSize are always injected automatically — never pass them yourself in setParams.


What's next?

On this page