Lightweight reactive store implementation with subscriptions and persistence support. Svelte store contract compatible.
Features:
equal) and custom serializers for non-JSON-safe valuesset (consistent notification ordering across subscribers)Symbol.dispose support — subscriptions work with usingdeno add jsr:@marianmeres/store
npm install @marianmeres/store
import { createStore, createDerivedStore } from "@marianmeres/store";
// Create a writable store
const count = createStore(0);
// Subscribe to changes (callback is called immediately with current value)
const unsub = count.subscribe(val => console.log(val)); // logs: 0
// Update the store
count.set(1); // logs: 1
count.update(n => n + 1); // logs: 2
// Get current value without subscribing
console.log(count.get()); // 2
unsub(); // stop receiving updates
Create computed values from other stores. A derived store accepts either a single source store or an array of sources:
const firstName = createStore("John");
const lastName = createStore("Doe");
// Single source
const upper = createDerivedStore(firstName, (name) => name.toUpperCase());
// Multiple sources
const fullName = createDerivedStore(
[firstName, lastName],
([first, last]) => `${first} ${last}`
);
fullName.subscribe(console.log); // logs: "John Doe"
firstName.set("Jane"); // logs: "Jane Doe"
Asynchronous derivation (use the set callback — the deriveFn must declare two explicit parameters):
const search = createStore("");
const results = createDerivedStore<Result[]>([search], ([query], set) => {
fetchResults(query).then(data => set!(data));
}, { initialValue: [] });
Derived stores are lazy: source stores are only subscribed once the derived store itself gains a subscriber. The unsubscribe function returned by subscribe() is idempotent — safe to call multiple times.
Automatically persist store values to storage:
import { createStorageStore, createStoragePersistor, createStore } from "@marianmeres/store";
// Simple: auto-persisted store
const prefs = createStorageStore("preferences", "local", { theme: "dark" });
prefs.set({ theme: "light" }); // automatically saved to localStorage
// Advanced: manual persistor with error handling
const persistor = createStoragePersistor<number>("counter", "local");
const counter = createStore(persistor.get() ?? 0, {
persist: persistor.set,
onPersistError: (e) => console.error("Storage failed:", e)
});
Storage types: "local" (localStorage), "session" (sessionStorage), "memory" (in-memory Map).
Custom serializers are supported for non-JSON-safe values (Date, Map, Set, BigInt, encrypted payloads, etc.):
const persistor = createStoragePersistor<Date>("when", "local", {
serialize: (v) => (v as Date).toISOString(),
deserialize: (s) => new Date(s),
});
By default, set/update notify subscribers only when the new value differs by strict equality (===). Pass a custom equal comparator when you want structural comparison:
const store = createStore({ count: 0 }, {
equal: (a, b) => JSON.stringify(a) === JSON.stringify(b),
});
store.set({ count: 0 }); // no notification — same shape
store.set({ count: 1 }); // notifies
using)The unsubscribe function returned by subscribe() implements Symbol.dispose, so subscriptions can be tied to a block with the using statement (TypeScript ES2024):
{
using sub = store.subscribe(v => console.log(v));
// ...work...
} // sub is automatically disposed here
Calling the unsubscribe directly (as a function) continues to work and remains idempotent.
See API.md for complete API documentation. See CHANGELOG.md for migration notes between versions.