Integrations
Vanilla / TypeScript
Use Stunk without any framework — pure reactive state in plain JavaScript or TypeScript.
Stunk has zero dependencies and no framework requirements. Everything works in plain JavaScript or TypeScript — chunk, computed, select, batch, and all middleware.
Basic state
import { chunk } from "stunk";
const count = chunk(0);
count.get(); // 0
count.peek(); // read without tracking
count.set(1);
count.set((prev) => prev + 1);
count.reset();Subscribing to changes
subscribe() fires on every change — not on initial subscribe. Returns an unsubscribe function.
const count = chunk(0);
const unsubscribe = count.subscribe((value) => {
document.querySelector("#count").textContent = String(value);
});
count.set(1); // DOM updates
count.set(2); // DOM updates
unsubscribe();In v3, subscribe() no longer fires immediately with the current value on
registration. Call chunk.get() directly for the initial value.
Deriving state
import { chunk } from "stunk";
const price = chunk(100);
const discounted = price.derive((p) => p * 0.9);
discounted.subscribe((value) => {
document.querySelector("#price").textContent = `$${value}`;
});
price.set(200); // discounted → 180, DOM updatesSelecting from objects
import { chunk, select } from "stunk";
const user = chunk({ name: "Fola", age: 25, role: "admin" });
const userName = select(user, (u) => u.name);
userName.subscribe((name) => {
document.querySelector("#name").textContent = name;
});
user.set((prev) => ({ ...prev, age: 26 })); // no DOM update
user.set((prev) => ({ ...prev, name: "Tunde" })); // DOM updatesComputed from multiple chunks
import { chunk, computed } from "stunk";
const price = chunk(100);
const quantity = chunk(3);
const total = computed(() => price.get() * quantity.get());
total.subscribe((value) => {
document.querySelector("#total").textContent = `$${value}`;
});
price.set(50); // total → 150
quantity.set(10); // total → 500Batch updates
import { chunk, batch } from "stunk";
const firstName = chunk("Fola");
const lastName = chunk("Ade");
// With batch — one notification after both updates
batch(() => {
firstName.set("Tunde");
lastName.set("Bello");
});Async state
import { asyncChunk } from "stunk/query";
const postsChunk = asyncChunk(async () => {
const res = await fetch("/api/posts");
return res.json();
});
postsChunk.subscribe(({ loading, error, data }) => {
if (loading) {
document.querySelector("#posts").textContent = "Loading...";
return;
}
if (error) {
document.querySelector("#posts").textContent = `Error: ${error.message}`;
return;
}
document.querySelector("#posts").innerHTML = data
.map((p) => `<li>${p.title}</li>`)
.join("");
});
document.querySelector("#reload").addEventListener("click", () => {
postsChunk.reload();
});Middleware
import { chunk } from "stunk";
import { logger, nonNegativeValidator } from "stunk/middleware";
const score = chunk(0, {
middleware: [logger(), nonNegativeValidator],
});
score.set(10); // logs and applies
score.set(-1); // throws, value unchangedhistory
import { chunk } from "stunk";
import { history } from "stunk/middleware";
const text = chunk("");
const editor = history(text);
editor.set("Hello");
editor.set("Hello World");
editor.undo(); // "Hello"
editor.redo(); // "Hello World"
editor.reset(); // "" — clears history toopersist
import { chunk } from "stunk";
import { persist } from "stunk/middleware";
const theme = chunk<"light" | "dark">("light");
const persistedTheme = persist(theme, { key: "app-theme" });
persistedTheme.set("dark"); // saved to localStorage
// Next page load — automatically restored to "dark"
persistedTheme.clearStorage(); // remove from localStorageCleanup
Call destroy() when a chunk is no longer needed:
const count = chunk(0);
count.subscribe(console.log);
count.destroy(); // clears all subscribersIn single-page apps, long-lived global chunks rarely need to be destroyed.
destroy() is most useful for short-lived chunks created dynamically — like
per-item state in a list.