store
Signature
store(id?: string, options?: StoreOptions): Store
valdres
Create a store to hold all state
Creates a store instance that holds all atom and selector state and manages subscriptions. The store is the central hub for reading, writing, and subscribing to state.
Usage
import { atom, store } from "valdres"
const myStore = store()
const countAtom = atom(0)
Every store exposes a stable, read-only id. Pass one explicitly when an
adapter or application needs to address the store later; generated IDs are
unique within the loaded Valdres instance.
const checkoutStore = store("checkout")
checkoutStore.id // "checkout"
The Store facade exposes operations and identity, not its mutable graph/cache runtime. Framework and tooling integrations use Valdres' versioned adapter entrypoint instead of reaching through the public Store object.
Options
The options bag is exported as StoreOptions, so a wrapper can accept and
forward it without re-declaring the shape. It also carries id, which the
store(options) form reads instead of a positional first argument.
| Option | Type | Description |
|---|---|---|
batchUpdates | boolean | Batch sequential set() calls within a tick into one notification pass (recommended for React) |
enumerable | boolean | Retain values enumerably so store.snapshot() can list current state |
schemaValidation | boolean | Validate atom/selector values against their schema (off by default). Inherited by scoped stores. See Schema Validation. |
With batchUpdates, a store and its scopes share pending writes for the tick.
Synchronous descendant reads see pending ancestor values while scope-local
shadows remain isolated from ancestors; subscriber notifications stay deferred
until the batch commits.
// Enable schema validation (e.g. outside production)
const devStore = store({ schemaValidation: import.meta.env.DEV })
Reading state
myStore.get(countAtom) // 0
Writing state
// Set directly
myStore.set(countAtom, 42)
// Update based on previous value
myStore.set(countAtom, prev => prev + 1)
Synchronous values and updater functions return the written value. Promise or
promise-returning-updater inputs return the normalized Promise, which settles
when the async write does:
const count = myStore.set(countAtom, 42) // number
const pendingCount = myStore.set(countAtom, Promise.resolve(43)) // Promise<number>
await pendingCount
Subscribing to changes
Subscription callbacks are notifications. Read the current value from the store when the callback runs:
const unsub = myStore.sub(countAtom, () => {
console.log("count changed:", myStore.get(countAtom))
})
// Later: stop listening
unsub()
Transactions
Batch multiple updates so subscribers only fire once:
myStore.txn(({ set }) => {
set(atomA, 1)
set(atomB, 2)
// Subscribers fire once after the transaction completes
})
Valdres owns commit and rollback: a successful callback commits once, while a
callback that throws discards every staged write. Callback transactions
intentionally expose no manual commit() or backing store data.
The transaction hands back whatever the callback returned, so a value read against its view can come straight out:
const total = myStore.txn(txn => {
txn.set(priceAtom, 100)
return txn.get(priceAtom) * quantity
})
Promise and thenable callbacks are rejected at runtime — a transaction commits synchronously, so it cannot await anything.
Disposing request stores
Global atoms remember each store that has materialized them so writes can fan out synchronously. Dispose short-lived stores—such as one store per SSR request or background job—when their work finishes:
const requestStore = store()
try {
return await renderRequest(requestStore)
} finally {
requestStore.dispose()
}
dispose() is terminal. It drains the store and all descendant scopes of active
subscriptions, atom mounts and their timers, onChange / onCommitEnd
listeners, pending batched writes, async selector work, and registrations on the
global atoms they touched. Cleanup stays proportional to lifecycle resources the
store actually created.
Detached scopes are disposed automatically when their last consumer calls
detach(). Every later operation on a disposed store throws
StoreDisposedError; calling dispose() again is a no-op. Create a new store
for later work.
Releasing your own resources
Valdres cleans up what it owns. For anything you keep alongside a store — a
cache, a connection, a timer — register it with onDispose, which runs when the
store is disposed, when an ancestor is disposed, or, for a scope, when its last
detach() lands:
const requestStore = store()
const connection = openConnection()
requestStore.onDispose(() => connection.close())
It returns a function that cancels the registration. Each call registers
independently, so the same function passed twice runs twice and each canceller
removes only its own. The store is already terminal inside the callback — every
operation on it throws — so read what you need beforehand and close over it.
Every callback runs even if an earlier one throws, and the first error reaches
whoever called dispose().
Per-atom setup belongs in an atom's onMount instead, whose cleanup tracks
subscribers rather than the store's lifetime.
Scopes
scope(scopeId) opens a child store that inherits everything from this one and
keeps its own writes local — see the
scoped stores guide. hasScope answers whether one
exists right now, which is otherwise only observable by catching what the
callback form throws:
const draft = myStore.scope("draft")
myStore.hasScope("draft") // true
draft.detach()
myStore.hasScope("draft") // false — that was the last lease
It reports this store's own children, not a search of the tree; depth composes
through scope() rather than a path argument:
myStore.scope("a", s => s.hasScope("b"))
The global store
valdres exports a shared globalStore — the store that global atoms
synchronize through. Import it when you need store access outside of a component
(scripts, tests, workers, cross-framework code):
import { globalStore } from "valdres"
globalStore.get(myAtom)
globalStore.set(myAtom, 1)
globalStore is a process-wide singleton that global atoms use for
synchronization. Calling globalStore.dispose() throws. Only dispose stores
that your application creates with store().Inside a component you normally read and write through your framework's provider/context instead — see createValdres.
See also
- createValdres — provide a store to your component tree
- useStore — access the current store in your components
- Schema Validation — opt-in runtime validation via
the
schemaValidationoption