stunk
| Install | |
|---|---|
npm install stunk |
|
| Latest Version: | 3.1.1 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Aug 4, 2026 |
| Links: | GitHub · npm |
Stunk
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.
- Pronunciation: Stunk (a playful blend of "state" and "chunk")
Features
- 🚀 Lightweight — 3.32kB gzipped, zero dependencies
- ⚛️ Atomic — break state into independent chunks
- 🔄 Reactive — fine-grained updates, only affected components re-render
- 🧮 Auto-tracking computed — no dependency arrays, just call
.get() - 🌐 Async & Query layer — loading, error, caching, deduplication, pagination built in
- 🔁 Mutations — reactive POST/PUT/DELETE with automatic cache invalidation
- 📦 Batch updates — group multiple updates into one render
- 🔌 Middleware — logging, persistence, validation — plug anything in
- ⏱️ Time travel — undo/redo state changes
- 🔍 TypeScript first — full type inference, no annotations needed
Installation
npm install stunk
# or
yarn add stunk
# or
pnpm add stunk
Core State
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
Computed — auto dependency tracking
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
Async & Query
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 }
Mutations
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");
Global Query Config
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),
},
});
Middleware
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!"
History (undo/redo)
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
Persist
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
React
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");
};
}
Package exports
| 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 |
Contributing
Contributions are welcome — open a pull request or issue.
License
MIT
Related Packages
A zero-dependency event bus and global state store for Vue, React, Mini Programs...
A Vue composable for managing async operations with reactive state, cancellation...
Vue 3 API integration utilities with reactive state management for Graduate Coll...
Vue bindings for @cavulsqa/reactive-db: a reactive query composable, Framework7...
Version History
| Version | Released | License |
|---|---|---|
| 3.1.1 | MIT | |
| 3.1.0 | MIT | |
| 3.0.6 | MIT | |
| 3.0.5 | MIT | |
| 3.0.4 | MIT | |
| 3.0.3 | MIT | |
| 3.0.2 | MIT | |
| 3.0.1 | MIT | |
| 3.0.0 | MIT | |
| 2.8.1 | MIT | |
| 2.8.0 | MIT | |
| 2.7.1 | MIT | |
| 2.6.0 | MIT | |
| 2.5.0 | MIT | |
| 2.4.0 | MIT | |
| 2.3.1 | MIT | |
| 2.3.0 | MIT | |
| 2.2.3 | MIT | |
| 2.2.2 | MIT | |
| 2.2.1 | MIT | |
| 2.2.0 | MIT | |
| 2.1.0 | MIT | |
| 2.0.0 | MIT | |
| 1.4.8 | MIT | |
| 1.4.7 | MIT | |
| 1.4.6 | MIT | |
| 1.4.5 | MIT | |
| 1.2.5 | MIT | |
| 1.2.4 | MIT | |
| 1.2.3 | MIT |
Showing the latest 30 of 40. See every release on npm
Pre-releases (20)
| Version | Released | License |
|---|---|---|
| 3.0.0-rc.15 | MIT | |
| 3.0.0-rc.14 | MIT | |
| 3.0.0-rc.13 | MIT | |
| 3.0.0-rc.12 | MIT | |
| 3.0.0-rc.11 | MIT | |
| 3.0.0-rc.10 | MIT | |
| 3.0.0-rc.9 | MIT | |
| 3.0.0-rc.8 | MIT | |
| 3.0.0-rc.7 | MIT | |
| 3.0.0-rc.6 | MIT | |
| 3.0.0-rc.5 | MIT | |
| 3.0.0-rc.4 | MIT | |
| 3.0.0-rc.3 | MIT | |
| 3.0.0-rc.2 | MIT | |
| 3.0.0-rc.1 | MIT | |
| 3.0.0-rc | MIT | |
| 3.0.0-beta.4 | MIT | |
| 3.0.0-beta.3 | MIT | |
| 3.0.0-beta.2 | MIT | |
| 3.0.0-beta.1 | MIT |