StunkStunk

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 role

The key difference is useShallowEqualselect() 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 fine

Does 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: 10

This 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?

SituationHook
Read + write a chunkuseChunk
Read-only (derived, computed, select, or plain)useChunkValue
Async stateuseAsyncChunk
Infinite scrolluseInfiniteAsyncChunk

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 changes

Can 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 automatically

Async

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, ignores staleTime
  • refresh() — only fetches if data is stale, respects staleTime

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";

On this page