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.

OptionTypeDescription
batchUpdatesbooleanBatch sequential set() calls within a tick into one notification pass (recommended for React)
enumerablebooleanRetain values enumerably so store.snapshot() can list current state
schemaValidationbooleanValidate 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.

Warning
Selectors that depend on atoms updated in a transaction will only recompute once, after all updates are applied. This is the intended behavior for performance.

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)

Do not dispose the global store
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 setValdresContext.

See also