StunkStunk
Integrations

React

Official React integration for Stunk — hooks for reading, writing, and subscribing to chunks.

Stunk's React integration is available from stunk/react. It provides hooks that subscribe to chunks and trigger re-renders only when the relevant value changes.

import {
  useChunk,
  useChunkValue,
  useAsyncChunk,
  useInfiniteAsyncChunk,
  useMutation,
} from "stunk/react";

useChunk

The primary hook for reading and writing a chunk. Returns [value, set, reset, destroy].

import { chunk } from "stunk";
import { useChunk } from "stunk/react";

const counter = chunk(0);

function Counter() {
  const [count, setCount, resetCount] = useChunk(counter);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={resetCount}>Reset</button>
    </div>
  );
}

With a selector

Pass an optional selector to subscribe to a slice of the value. The component only re-renders when the selected slice changes:

const user = chunk({ name: "Fola", age: 25, role: "admin" });

function UserName() {
  const [name, setUser] = useChunk(user, (u) => u.name);
  // only re-renders when name changes
}

Return value

const [value, set, reset, destroy] = useChunk(chunk, selector?);
TypeDescription
valueTCurrent chunk value (or selected slice)
set(value | updater) => voidUpdate the chunk
reset() => voidReset to initial value
destroy() => voidDestroy the chunk and clear all subscribers

useChunkValue

Read-only version of useChunk. Use this when a component only needs to read — no setter needed.

import { useChunkValue } from "stunk/react";

const theme = chunk<"light" | "dark">("light");

function ThemeDisplay() {
  const currentTheme = useChunkValue(theme);
  return <p>Theme: {currentTheme}</p>;
}

Also accepts a selector:

const user = chunk({ name: "Fola", age: 25 });

function UserAge() {
  const age = useChunkValue(user, (u) => u.age);
  return <p>Age: {age}</p>;
}

This is the preferred hook for derived chunks, computed chunks, and selectors:

const price = chunk(100);
const quantity = chunk(3);
const total = computed(() => price.get() * quantity.get());
const discounted = price.derive((p) => p * 0.9);

function Summary() {
  const totalValue = useChunkValue(total);
  const discountedValue = useChunkValue(discounted);

  return (
    <p>
      Total: ${totalValue} (discounted: ${discountedValue})
    </p>
  );
}

useAsyncChunk

Subscribes to an async chunk and returns its full state with all async methods.

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((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Options

OptionTypeDescription
paramsPartial<P>Params to pass to the fetcher. Re-fetches automatically when changed.
fetchOnMountbooleanForce fetch on mount for param-less chunks (default: false)
onSuccess(data: T) => voidCalled after every successful fetch. Has full React context access.
onError(error: E) => voidCalled when a fetch fails. Has full React context access.

initialParams is deprecated — use params instead. It will be removed in v3 stable.

Hook-level callbacks

onSuccess and onError at the hook level have full access to React context — navigate, setState, anything React. This is the correct place for navigation after fetch, unlike asyncChunk-level callbacks which are defined at module scope and have no React context.

function ProfilePage() {
  const navigate = useNavigate();
  const [name, setName] = useState("");

  const { data, loading } = useAsyncChunk(profileChunk, {
    onSuccess: (data) => {
      if (!data) return navigate("/login");
      setName(data.name);
    },
    onError: () => navigate("/error"),
  });
}

Return value

TypeDescription
dataT | nullResolved data
loadingbooleantrue while fetching
errorE | nullError from last failed fetch
lastFetchednumber | undefinedTimestamp of last successful fetch
isPlaceholderDatabooleantrue when showing stale data during refetch
reload(params?) => Promise<void>Force refetch
refresh(params?) => Promise<void>Refetch if stale
mutate(mutator) => voidUpdate data directly
reset() => voidReset to initial state
setParams(params) => voidUpdate params and refetch (if chunk accepts params)
clearParams() => voidClear all params and refetch (if chunk accepts params)
paginationPaginationStateCurrent pagination state (if paginated)
nextPage() => Promise<void>Load next page (if paginated)
prevPage() => Promise<void>Load previous page (if paginated)
goToPage(page) => Promise<void>Jump to a page (if paginated)
resetPagination() => Promise<void>Reset to page 1 (if paginated)

useAsyncChunk automatically calls cleanup() when the component unmounts — clearing any polling intervals or cache timers.


useMutation

Subscribes to a mutation and returns mutate, loading, error, and data.

import { mutation } from "stunk/query";
import { useMutation } from "stunk/react";

const createPostMutation = mutation(
  async (payload: { title: string }) => apiCreatePost(payload),
  { invalidates: [postsChunk] },
);

function CreatePost() {
  const { mutate, loading, error } = useMutation(createPostMutation);

  async function handleSubmit(title: string) {
    const { error } = await mutate({ title });
    if (!error) toast.success("Post created");
  }

  return (
    <button onClick={() => handleSubmit("My post")} disabled={loading}>
      {loading ? "Creating..." : "Create"}
    </button>
  );
}

Hook-level callbacks

Like useAsyncChunk, useMutation supports onSuccess and onError at the hook level for navigation and React state updates:

function CreateTenant() {
  const navigate = useNavigate();

  const { mutate, loading } = useMutation(createTenantMutation, {
    onSuccess: (tenant) => {
      toast.success("Tenant added");
      navigate(`/tenants/${tenant.id}`);
    },
    onError: (err) => toast.error(err.message),
  });
}

Options

OptionTypeDescription
onSuccess(data: T) => voidCalled after mutation succeeds. Has React context.
onError(error: E) => voidCalled when mutation fails. Has React context.

Return value

TypeDescription
mutate(payload) => Promise<Result>Trigger the mutation
loadingbooleantrue while the mutation is in progress
errorE | nullError from the last failed mutation
dataT | nullData from the last successful mutation
reset() => voidReset state to idle

useInfiniteAsyncChunk

Wraps useAsyncChunk for infinite scroll. 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((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
      <div ref={observerTarget} />
      {isFetchingMore && <p>Loading more...</p>}
      {!hasMore && <p>You've reached the end.</p>}
    </div>
  );
}

Options

OptionTypeDefaultDescription
autoLoadbooleantrueAuto-load next page when sentinel is visible
thresholdnumber1.0Intersection observer threshold
initialParamsobjectInitial params (excluding page and pageSize)

Summary

HookUse case
useChunkRead + write a chunk
useChunkValueRead-only — also for derived/computed/select
useAsyncChunkAsync state with loading/error/data
useMutationPOST, PUT, DELETE with cache invalidation
useInfiniteAsyncChunkInfinite scroll with auto-load

On this page