StunkStunk
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 updates

Selecting 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 updates

Computed 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 → 500

Batch 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 unchanged

history

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 too

persist

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 localStorage

Cleanup

Call destroy() when a chunk is no longer needed:

const count = chunk(0);
count.subscribe(console.log);

count.destroy(); // clears all subscribers

In 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.


What's next?

On this page