UNPKG

stunk

Version:

Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue(WIP), Svelte(Coming soon), and Vanilla JS/TS a

256 lines (184 loc) 6.59 kB
# 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 ( <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](https://github.com/I-am-abdulazeez/stunk/pulls) or [issue](https://github.com/I-am-abdulazeez/stunk/issues). ## License MIT