Stunk is a lightweight, framework-agnostic state management library built on atomic state principles. Break state into independent chunks — each one reactive, composable, and self-contained.
.get()npm install stunk
# or
yarn add stunk
# or
pnpm add stunk
import { chunk } from "stunk";
const count = chunk(0);
count.get(); // 0
count.set(10); // set directly
count.set((prev) => prev + 1); // updater function
count.peek(); // read without tracking dependencies
count.reset(); // back to 0
count.destroy(); // clear all subscribers
No dependency arrays. Any chunk whose .get() is called inside the function is tracked automatically:
import { chunk, computed } from "stunk";
const price = chunk(100);
const quantity = chunk(3);
const total = computed(() => price.get() * quantity.get());
total.get(); // 300
price.set(200);
total.get(); // 600 — recomputed automatically
Use .peek() to read without tracking:
const taxRate = chunk(0.1);
const subtotal = computed(() => price.get() * (1 + taxRate.peek()));
// only recomputes when price changes
import { asyncChunk } from "stunk/query";
const userChunk = asyncChunk(
async ({ id }: { id: number }) => fetchUser(id),
{
key: "user", // deduplicates concurrent requests
keepPreviousData: true, // no UI flicker on param changes
staleTime: 30_000,
onError: (err) => toast.error(err.message),
}
);
userChunk.setParams({ id: 1 });
// { loading: true, data: null, error: null }
// { loading: false, data: { id: 1, name: "..." }, error: null }
Reactive POST/PUT/DELETE — one function, always safe to await or fire and forget:
import { mutation } from "stunk/query";
const createPost = mutation(
async (data: NewPost) => fetchAPI("/posts", { method: "POST", body: data }),
{
invalidates: [postsChunk], // auto-reloads on success
onSuccess: () => toast.success("Created!"),
onError: (err) => toast.error(err.message),
}
);
// Fire and forget — safe
createPost.mutate({ title: "Hello" });
// Await for local control — no try/catch needed
const { data, error } = await createPost.mutate({ title: "Hello" });
if (!error) router.push("/posts");
Set defaults once for all async chunks and mutations:
import { configureQuery } from "stunk/query";
configureQuery({
query: {
staleTime: 30_000,
retryCount: 3,
onError: (err) => toast.error(err.message),
},
mutation: {
onError: (err) => toast.error(err.message),
},
});
import { chunk } from "stunk";
import { logger, nonNegativeValidator } from "stunk/middleware";
const score = chunk(0, {
middleware: [logger(), nonNegativeValidator]
});
score.set(10); // logs: "Setting value: 10"
score.set(-1); // throws: "Value must be non-negative!"
import { chunk } from "stunk";
import { history } from "stunk/middleware";
const count = chunk(0);
const tracked = history(count);
tracked.set(1);
tracked.set(2);
tracked.undo(); // 1
tracked.redo(); // 2
tracked.reset(); // 0 — clears history too
import { chunk } from "stunk";
import { persist } from "stunk/middleware";
const theme = chunk<"light" | "dark">("light");
const saved = persist(theme, { key: "theme" });
saved.set("dark"); // saved to localStorage
saved.clearStorage(); // remove from localStorage
import { chunk, computed } from "stunk";
import { useChunk, useChunkValue, useAsyncChunk, useMutation } from "stunk/react";
const counter = chunk(0);
const double = computed(() => counter.get() * 2);
function Counter() {
const [count, setCount] = useChunk(counter);
const doubled = useChunkValue(double);
return (
<div>
<p>Count: {count} — Doubled: {doubled}</p>
<button onClick={() => setCount((n) => n + 1)}>+</button>
</div>
);
}
// Async
function PostList() {
const { data, loading, error } = useAsyncChunk(postsChunk);
if (loading) return <p>Loading...</p>;
return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// Mutation
function CreatePostForm() {
const { mutate, loading, error } = useMutation(createPost);
const handleSubmit = async (data: NewPost) => {
const { error } = await mutate(data);
if (!error) router.push("/posts");
};
}
| Import | Contents |
|---|---|
stunk |
chunk, computed, select, batch, isChunk, and more |
stunk/react |
useChunk, useChunkValue, useAsyncChunk, useInfiniteAsyncChunk, useMutation |
stunk/query |
asyncChunk, infiniteAsyncChunk, combineAsyncChunks, mutation, configureQuery |
stunk/middleware |
history, persist, logger, nonNegativeValidator |
Contributions are welcome — open a pull request or issue.
MIT