Getting Started with Store#

Prerequisites

Before continuing, make sure you're familiar with: SignalX Core basics, Signals, Components

@sigx/store is centralized, reactive state built on the same signal core that powers your components — with a surface that is flat and signal-first. You describe state, actions, and derived values in one setup function; whatever you return becomes the store. State reads and writes as plain values (store.count), computeds read as plain read-only values (store.total), actions are called directly (store.increment()) — and any of them read inside a render function re-renders that component automatically. No selector layer, no manual subscriptions, no .value at the call site.

Here is the whole thing, live — edit it:

TSX
Loading preview...

That's defineStore(name, setup): the setup receives a context with defineState and defineActions, and the spread signals you return become the store's public state. Calling the returned factory anywhere yields a live instance.

Async without the boilerplate#

Real stores talk to servers. Every action carries a reactive pending flag — true while any invocation is in flight — so a save button or spinner is one property read, with no isLoading flags to maintain by hand. Try adding a todo (the action fakes a 700 ms API call):

TSX
Loading preview...

Notice what you didn't write: no loading flag, no try/finally to reset it, no selector to subscribe the list. store.add.pending is count-based across overlapping calls, the computed unwraps as a plain store.remaining, and patch applies the multi-key update in a single reactivity flush.

The mental model in five lines#

  • defineStore(name, setup, lifetime?) returns a factory function. By convention it's named useXxxStore, but it's an ordinary function — calling it creates (or resolves, per the lifetime) an instance. There is no useStore export.
  • Inside setup, defineState(obj) returns { state, signals, events, patch }. Mutate state freely (it's deeply reactive); the signals keys you return become the store's public state — unreturned keys stay private.
  • defineActions(obj) wraps your functions; each keeps its exact signature and carries pending plus onDispatching / onDispatched / onFailure events.
  • The instance is an unwrap proxy: signals and computeds read as plain values; meta lives behind $ ($id, $patch, $events, $dispose).
  • Created inside a component, an instance is tied to the component lifecycle and disposes on unmount.

One convention to internalize early: don't destructure the store. const { count } = store snapshots the value and loses reactivity. Keep reads on the store (store.count) or use storeToSignals.

What makes this store different#

  • A flat, fully-typed surface. Public state is exactly the signals you return — $patch and $events are typed from them, action signatures flow through unchanged. See TypeScript.
  • pending built in. Every action knows when it's in flight, with sound error semantics (sync errors re-throw; async calls return the original promise). See Actions & Async.
  • Lazy events. Per-key change events (store.$events.count) cost nothing until someone subscribes — the deep watcher behind a key only runs while it has subscribers. See Events & Observability.
  • Real lifetimes. 'singleton' | 'scoped' | 'transient' are honored by the core DI layer, with defineProvide subtree overrides and automatic disposal. See Lifetimes & DI.
  • Persistence in the box. @sigx/store/persist handles sync and async storage, versioned migrations, and hydration races. See Persistence.
  • Devtools auto-discovery. Every store's state, action, and custom events are registered topics in the core inspection registry — tooling can find and tap them by wildcard, no wiring. See Events & Observability.
  • Composable and pluggable. Setup composables (like persist) and the global onStoreCreated hook cover cross-cutting concerns. See Composables & Plugins.

Next steps#