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?);| Type | Description | |
|---|---|---|
value | T | Current chunk value (or selected slice) |
set | (value | updater) => void | Update the chunk |
reset | () => void | Reset to initial value |
destroy | () => void | Destroy 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
| Option | Type | Description |
|---|---|---|
params | Partial<P> | Params to pass to the fetcher. Re-fetches automatically when changed. |
fetchOnMount | boolean | Force fetch on mount for param-less chunks (default: false) |
onSuccess | (data: T) => void | Called after every successful fetch. Has full React context access. |
onError | (error: E) => void | Called 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
| Type | Description | |
|---|---|---|
data | T | null | Resolved data |
loading | boolean | true while fetching |
error | E | null | Error from last failed fetch |
lastFetched | number | undefined | Timestamp of last successful fetch |
isPlaceholderData | boolean | true when showing stale data during refetch |
reload | (params?) => Promise<void> | Force refetch |
refresh | (params?) => Promise<void> | Refetch if stale |
mutate | (mutator) => void | Update data directly |
reset | () => void | Reset to initial state |
setParams | (params) => void | Update params and refetch (if chunk accepts params) |
clearParams | () => void | Clear all params and refetch (if chunk accepts params) |
pagination | PaginationState | Current 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
| Option | Type | Description |
|---|---|---|
onSuccess | (data: T) => void | Called after mutation succeeds. Has React context. |
onError | (error: E) => void | Called when mutation fails. Has React context. |
Return value
| Type | Description | |
|---|---|---|
mutate | (payload) => Promise<Result> | Trigger the mutation |
loading | boolean | true while the mutation is in progress |
error | E | null | Error from the last failed mutation |
data | T | null | Data from the last successful mutation |
reset | () => void | Reset 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
| Option | Type | Default | Description |
|---|---|---|---|
autoLoad | boolean | true | Auto-load next page when sentinel is visible |
threshold | number | 1.0 | Intersection observer threshold |
initialParams | object | — | Initial params (excluding page and pageSize) |
Summary
| Hook | Use case |
|---|---|
useChunk | Read + write a chunk |
useChunkValue | Read-only — also for derived/computed/select |
useAsyncChunk | Async state with loading/error/data |
useMutation | POST, PUT, DELETE with cache invalidation |
useInfiniteAsyncChunk | Infinite scroll with auto-load |