StunkStunk
Async

Async Chunk

Handle async operations with built-in loading, error, and data states.

asyncChunk() wraps an async fetcher function and gives you reactive loading, error, data, and lastFetched states out of the box — no manual state juggling needed.

import { asyncChunk } from "stunk/query";

const userChunk = asyncChunk(async () => {
  const res = await fetch("/api/user");
  return res.json();
});

Fetchers with no parameters are called automatically on creation. Fetchers that take parameters wait for setParams() or reload().


State shape

PropertyTypeDescription
dataT | nullThe resolved value, or null before first fetch
loadingbooleantrue while a fetch is in progress
errorE | nullThe error if the last fetch failed, otherwise null
lastFetchednumber | undefinedTimestamp of the last successful fetch
isPlaceholderDatabooleantrue when showing previous data while a new fetch is in progress

Options

OptionTypeDefaultDescription
keystringautoDeduplication key — concurrent calls share one request
enabledboolean | ((params: Partial<P>) => boolean)trueDisable fetching until ready — receives current params
initialDataTnullSeed data before the first fetch
keepPreviousDatabooleanfalseShow previous data while refetching — no UI flicker
onSuccess(data: T) => voidCalled after every successful fetch
onError(error: E) => voidCalled when all retries are exhausted
retryCountnumber0Number of retries on failure
retryDelaynumber1000ms between retries
staleTimenumber0ms before data is considered stale
cacheTimenumber300_000ms to keep cache after last subscriber leaves
refetchIntervalnumberAuto-refetch interval in ms
refetchOnWindowFocusbooleanfalseRefetch when window regains focus
paginationobjectEnable pagination — see Pagination section below

All options except key, enabled, initialData, keepPreviousData, and pagination can be set globally via configureQuery() — per-chunk options always override global defaults.


Conditional fetching with enabled

enabled accepts a boolean or a function that receives the current params. This is the cleanest way to prevent fetching until required data is available:

// Static — never fetches
const chunk = asyncChunk(fetcher, { enabled: false });

// Dynamic — re-evaluated on every setParams call
const flatsByHouseChunk = asyncChunk(
  ({ houseId }: { houseId: string }) => apiGetFlatsByHouse(houseId),
  { enabled: ({ houseId }) => !!houseId },
);

The function receives the same params the fetcher will get — defined once on the chunk, no repetition at the hook callsite.


API reference

reload(params?)

Forces a fresh fetch, ignoring stale time.

await postsChunk.reload();
await postsChunk.reload({ category: "engineering" });

refresh(params?)

Smart refresh — only fetches if data is stale. Does nothing if data is still fresh.

await postsChunk.refresh();

mutate(mutator)

Update data directly without a network request. Useful for optimistic updates.

postsChunk.mutate((current) => [...(current ?? []), newPost]);

setParams(params)

Update fetcher params and trigger a fresh fetch. Pass null for a key to remove it.

postsChunk.setParams({ category: "engineering" });
postsChunk.setParams({ category: null }); // removes category key

clearParams()

Wipes all current params and refetches.

postsChunk.clearParams();

reset()

Resets to initial state and re-fetches from scratch.

postsChunk.reset();

cleanup()

Safe cleanup — only tears down intervals and listeners if no active subscribers remain.

postsChunk.cleanup();

forceCleanup()

Tears down all side effects regardless of active subscribers.

postsChunk.forceCleanup();

Request deduplication

Use key to deduplicate concurrent requests — if two components call reload() simultaneously on a chunk with the same key, only one request fires:

const userChunk = asyncChunk(fetchUser, { key: "user" });

userChunk.reload();
userChunk.reload(); // joins the same in-flight request

keepPreviousData

When params change, the default behavior shows null while loading. Set keepPreviousData: true to keep showing the previous data instead — no flash of empty state:

const postsChunk = asyncChunk(
  ({ category }: { category: string }) => fetchPosts(category),
  { keepPreviousData: true },
);

// While loading, isPlaceholderData is true and data still shows previous results
postsChunk.setParams({ category: "engineering" });

In React

import { asyncChunk } from "stunk/query";
import { useAsyncChunk } from "stunk/react";

const postsChunk = asyncChunk(async () => {
  const res = await fetch("/api/posts");
  return res.json() as Promise<Post[]>;
});

function PostList() {
  const { data, loading, error, reload } = useAsyncChunk(postsChunk);

  if (loading) return <p>Loading...</p>;
  if (error)
    return (
      <p>
        Error: {error.message} <button onClick={reload}>Retry</button>
      </p>
    );

  return (
    <ul>
      {data?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

What's next?

On this page