docs / Runtime

Store

A centralized reactive store with optional persistence, middleware, and derived stores.

Vesk includes a reactive store implementation inspired by libraries like Zustand, built on top of Vesk's fine-grained reactivity system. The store provides a centralized state management solution with optional persistence, middleware, and derived stores.

Creating a Store

Stores are created with the `createStore` function from `@vesk/runtime/store`.

app/store/counter.ts
									
										import { createStore } from '@vesk/runtime/store';

const store = createStore({ count: 0, theme: 'dark' }, {
  persist: true,
  storageKey: 'my-store'
});
									
								

Store API

The store returns an object with the following methods:

app/store/usage.ts
									
										// State access
store.get() // => { count: 0, theme: 'dark' }

// State updates
store.update({ count: 1 })
store.set({ count: 5, theme: 'light' })
store.update(prev => ({ count: prev.count + 1 }))

// Batch operations (prevents multiple subscriber calls)
store.batch(() => {
  store.update({ a: 1 })
  store.update({ b: 2 })
  store.set({ c: 3 })
})

// Reset to initial state
store.reset()

// Rehydrate with new initial state (keeps store identity)
store.rehydrate({ user: { id: 1, name: 'Ada' } })

// Clear state and persistence
store.clear()

// Subscribe to changes
const unsub = store.subscribe(state => console.log(state))

// Watch specific values
const unwatch = store.watch(
  s => s.count,
  (n, o) => console.log(`count: ${o} → ${n}`),
  (a, b) => a === b // shallow equality
)

// Middleware
const unuse = store.use((state, action) => console.log(`[${action}]`, state))

// Derived stores
const themeStore = store.derive(s => s.theme)
themeStore.subscribe(v => console.log('theme:', v))

// Debugging
console.log(store.toJSON()) // deep clone of state
									
								

Server Safety

The store is SSR-safe. When running on the server (where `window` is undefined), persistence and devtools features are automatically disabled, but all other functionality works identically.

Example Store

Build the store your app needs with `createStore`. The store is tree-shaken — it only ships in the client bundle when a component actually imports and uses it:

app/store/examples.ts
									
										import { createStore } from '@vesk/runtime/store'

const themeStore = createStore({ theme: 'dark' }, { persist: true, storageKey: 'theme-store' })
									
								

Path Store

For state that needs fine-grained path reactivity, causal provenance, or time travel, `createPathStore` from `@vesk/runtime/path-store` extends the same engine with three abilities `createStore` does not have: every value is a separate reactive dependency, every commit records which action wrote which paths, and `undo` / `redo` restore versions through structural sharing.

app/store/cart.ts
									
										import { createPathStore } from '@vesk/runtime/path-store';

const cart = createPathStore({ items: [{ price: 2 }, { price: 3 }], shipping: 5 });

// Fine-grained: reading one path re-runs only when that path changes.
const subtotal = cart.at('items.0.price').get();

// Derived nodes capture their selector's reads automatically.
const total = cart.derived('total', s =>
  s.items.reduce((sum, item) => sum + item.price, 0) + s.shipping
);

// Causal provenance — why does total equal what it equals?
cart.at('items.0.price').set(8, 'priceAdjusted');
console.log(cart.explain('total'));

// Time travel
cart.undo();
cart.redo();

// Type-checked paths: cart.at('items.0.done') is a type error.
									
								

Both stores support optional SSR persistence. Pass `{ persist: true, storageKey: 'cart' }` and the first server render reads the value back from a same-site cookie; the value is handed to the client *before paint* through an origin-served `/store-init.js?t=<token>` script (never an inline `<script>`, so a strict `script-src` CSP without `unsafe-inline` cannot block it). On write, the client mirrors the JSON to `localStorage` and the cookie so the next request re-renders pre-hydrated. `store.clear()` drops both. No app code is required — the adapter injects the script and drains a per-request key registry (`getPersistedStoreKeys`/`resetPersistedStoreKeys`) at stream close.