# 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 ```bash npm install stunk # or yarn add stunk # or pnpm add stunk ``` 📖 [Read the docs](https://stunk.dev) --- ## Core State ```ts 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: ```ts 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: ```ts const taxRate = chunk(0.1); const subtotal = computed(() => price.get() * (1 + taxRate.peek())); // only recomputes when price changes ``` --- ## Async & Query ```ts 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: ```ts 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: ```ts 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 ```ts 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) ```ts 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 ```ts 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 ```tsx 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 (

Count: {count} — Doubled: {doubled}

); } // Async function PostList() { const { data, loading, error } = useAsyncChunk(postsChunk); if (loading) return

Loading...

; return ; } // 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](https://github.com/I-am-abdulazeez/stunk/pulls) or [issue](https://github.com/I-am-abdulazeez/stunk/issues). ## License MIT