store
A small, framework-agnostic state container with auto-tracked derived values and optional built-in persist.
store is a small, framework-agnostic state container with auto-tracked derived values and simple built-in persist. It works with any framework, or no framework at all.
Motivation
Zustand’s setState does a shallow merge by default. This is a problem for nested state. It also has no built-in derived value: every selector runs again from scratch, with no caching.
Jotai’s atom model is nice, but jotai/utils alone has more than a dozen helper atoms. One simple idea (“a derived value”) grew into a large API.
TanStack Store has the best reactive core of the three. It uses an auto-tracked signal graph, so derived values only recompute when a real dependency changes. But subscribe() returns an object shaped like RxJS’s Subscription, not a plain function. It also has no built-in persistence.
store takes TanStack’s auto-tracked reactivity and Zustand’s simple, single-factory style. The public API stays small on purpose: createStore, derive, and nothing else. No middleware chain, no Provider, no atom zoo.
Features
- One factory per store, via
createStore(initialState, actionsFactory?, options?). Actions are created once, not on every render or every consumer. get()is stable — it returns the same reference across calls, until the state actually changes. Safe to use as a snapshot source for things like React’suseSyncExternalStore.settakes an updater only:set((prev) => partialOrFullState). The result is always shallow-merged. There is noset({ ... })shortcut, so there is no confusion between “merge” and “replace”.- Auto-tracked derived values, via
derive(deps, fn). The value is cached. It only recomputes when somethingfnactually read last time has changed.fncan read from any store, even one not listed indeps, and it will still track correctly. - Plain unsubscribe functions —
subscribe(...)returns() => void, not aSubscriptionobject. - Simple built-in persist — pass
{ persist: { key, storage } }tocreateStore.storageonly needsgetItemandsetItem, solocalStorageandsessionStoragework as-is; persist is a no-op during SSR without a custom storage adapter. - No required runtime dependencies.
- Full TypeScript inference for state, actions, and the values passed into
derive. You do not need to write generic types by hand.
Quick Start
import { createStore } from "@zap-studio/store";
const counter = createStore({ count: 0 }, (set, get) => ({
increment: () => set((s) => ({ count: s.count + 1 })),
}));
counter.get(); // { count: 0, increment: fn }
counter.getState(); // { count: 0 }
const unsubscribe = counter.subscribe((state) => console.log(state));
counter.get().increment(); // logs { count: 1, increment: fn }
Learn More
- Getting Started — install and build your first store step by step
set— the updater-only setter and how shallow merge worksderive— auto-tracked, cached derived values- Persist — save and load state through
localStorage-shaped storage
Runtime Support
| Runtime | Minimum version |
|---|---|
| Node.js | 18.0.0 |
| Bun | 1.0.0 |
| Deno | 1.42 |
| Cloudflare Workers | Any current release |
| Browsers | Chrome/Edge 98, Firefox 97, Safari 15.4 |
The package ships standard ESM only and uses no runtime-specific APIs. Deno 1.42 is the first release that can install packages from JSR (deno add jsr:@zap-studio/store).