FAQ
Frequently asked questions about Stunk.
General
How is Stunk different from Zustand?
Zustand uses a single store object — you define all your state in one place and select slices from it. Stunk uses atomic chunks — each piece of state lives independently and can be composed together.
// Zustand — one store
const useStore = create(() => ({ count: 0, name: "Fola" }));
const count = useStore((s) => s.count);
// Stunk — independent chunks
const count = chunk(0);
const name = chunk("Fola");Chunks don't know about each other by default. You compose them explicitly with computed(), batch(), or combineAsyncChunks(). This makes state easier to split across files, test in isolation, and reason about independently.
How is Stunk different from Jotai?
Jotai is also atom-based and the closest conceptually to Stunk. The key differences:
- Stunk works outside React — chunks are plain JS objects, not React atoms. Use them in Node, workers, or any framework.
- Stunk doesn't require a Provider — no
<Provider>wrapping your app. - Stunk has built-in async, history, and persistence — Jotai handles these through separate packages. Stunk ships them in the box.
- Stunk's API is smaller — four React hooks cover everything in v3.
How is Stunk different from Redux?
Redux requires actions, reducers, and a store setup before you can manage a single value. Stunk doesn't:
const count = chunk(0);
count.set((n) => n + 1);Redux is powerful for large teams and complex update logic. Stunk is for developers who want reactivity without the ceremony.
Does Stunk work with TypeScript?
Yes — fully. Types are inferred automatically from the initial value:
const count = chunk(0); // Chunk<number>
const status = chunk<"idle" | "loading" | "error">("idle");Can I use Stunk server-side?
The core stunk package — chunk, computed, select, batch — has no browser dependencies and works in any JS environment including Node.js, Deno, Cloudflare Workers, and SSR.
stunk/query is also SSR-safe — all window access is guarded. persist detects server environments automatically and disables itself gracefully.
React hooks (stunk/react) are browser/React-only, as expected.
Is Stunk production ready?
v2.8.1 is stable — 2.95kB gzipped, zero dependencies, and actively used in production. v3 is currently in alpha. For production use, stay on v2.8.1 until v3 stable ships.
Core Concepts
When should I use select() vs .derive()?
Use .derive() for simple value transformations:
const discounted = price.derive((p) => p * 0.9);Use select() when slicing a property off an object chunk and you want to avoid unnecessary updates:
const userName = select(user, (u) => u.name, { useShallowEqual: true });
// only updates when name changes, not age or roleThe key difference is useShallowEqual — select() supports it, .derive() doesn't.
When should I use computed() vs .derive()?
Use .derive() when your derived value depends on one chunk. Use computed() when it depends on multiple chunks:
const doubled = count.derive((n) => n * 2);
const total = computed(() => price.get() * quantity.get());Can I write to a derived chunk or selector?
No — they are read-only. Update the source chunk instead:
user.set((prev) => ({ ...prev, name: "Tunde" }));What happens if I pass null as an initial value?
In v3, null is a valid chunk value — you can initialize a chunk with null and set it to null at any time. undefined is the forbidden value:
chunk(null); // ✅ valid in v3
chunk(undefined); // ❌ throws "Initial value cannot be undefined."
const value = chunk<string | null>(null);
value.set("hello");
value.set(null); // back to null — works fineDoes subscribe fire immediately?
No — not in v3. The callback fires only on changes, not on initial subscribe. Call chunk.get() directly for the current value on mount:
const count = chunk(5);
count.subscribe((value) => console.log(value));
// nothing logged yet
count.set(10); // logs: 10This is a breaking change from v2 where subscribe fired immediately.
React
Do I need a Provider?
No. Chunks are defined outside components and accessed directly — no context, no Provider, no wrapping:
const count = chunk(0);
function Counter() {
const [value] = useChunk(count);
}Which hook should I use?
| Situation | Hook |
|---|---|
| Read + write a chunk | useChunk |
| Read-only (derived, computed, select, or plain) | useChunkValue |
| Async state | useAsyncChunk |
| Infinite scroll | useInfiniteAsyncChunk |
Will my component re-render on every chunk update?
Only when the value it subscribes to actually changes. With a selector, only when the selected slice changes:
const [name] = useChunk(user, (u) => u.name);
// re-renders only when user.name changesCan I share chunks across components?
Yes — define chunks at module scope and import them anywhere:
// store/counter.ts
export const counter = chunk(0);
// ComponentA and ComponentB both import counter
// and stay in sync automaticallyAsync
Does asyncChunk refetch on every mount?
No — it fetches once on creation. To refetch when a component mounts, use fetchOnMount in useAsyncChunk:
const { data } = useAsyncChunk(postsChunk, { fetchOnMount: true });What's the difference between reload() and refresh()?
reload()— always fetches, ignoresstaleTimerefresh()— only fetches if data is stale, respectsstaleTime
Where do I import asyncChunk from?
In v3, asyncChunk, infiniteAsyncChunk, and combineAsyncChunks live in stunk/query:
import {
asyncChunk,
infiniteAsyncChunk,
combineAsyncChunks,
} from "stunk/query";Middleware
Can I use multiple middleware on one chunk?
Yes — pass them in the middleware array inside the config. They run left to right:
const count = chunk(0, {
middleware: [logger(), nonNegativeValidator],
});Can middleware stop an update?
Yes — return undefined to cancel the update:
const noZero: Middleware<number> = (value) => {
if (value === 0) return undefined;
return value;
};Does history work with object chunks?
Yes — it records every set() call regardless of the value type:
import { history } from "stunk/middleware";
const user = chunk({ name: "Fola", age: 25 });
const tracked = history(user);
tracked.set({ name: "Tunde", age: 30 });
tracked.undo();
tracked.get(); // { name: "Fola", age: 25 }What changed in the middleware names?
In v3, withHistory is now history and withPersistence is now persist:
// v2
import { withHistory, withPersistence } from "stunk/middleware";
// v3
import { history, persist } from "stunk/middleware";