https://github.com/i-am-abdulazeez/stunk
A framework-agnostic state management library that keeps your app’s state clean and simple. It uses a fine-grained state model, breaking state into independent, manageable chunks.
https://github.com/i-am-abdulazeez/stunk
angular client-state-management queflow react solid state-management svelte typescript vue zustand-alternative
Last synced: about 1 month ago
JSON representation
A framework-agnostic state management library that keeps your app’s state clean and simple. It uses a fine-grained state model, breaking state into independent, manageable chunks.
- Host: GitHub
- URL: https://github.com/i-am-abdulazeez/stunk
- Owner: I-am-abdulazeez
- License: other
- Created: 2025-01-27T10:29:15.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-04-29T06:58:11.000Z (about 1 month ago)
- Last Synced: 2026-04-29T08:35:13.893Z (about 1 month ago)
- Topics: angular, client-state-management, queflow, react, solid, state-management, svelte, typescript, vue, zustand-alternative
- Language: TypeScript
- Homepage: https://stunk.dev/
- Size: 524 KB
- Stars: 168
- Watchers: 1
- Forks: 13
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Roadmap: ROADMAP.md
Awesome Lists containing this project
README
# 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}
setCount((n) => n + 1)}>+
);
}
// Async
function PostList() {
const { data, loading, error } = useAsyncChunk(postsChunk);
if (loading) return
Loading...
;
return - {data?.map(p =>
- {p.title} )}
}
// 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