Defining a Store
Everything in @sigx/store starts with defineStore. It takes a name (the logical store name, used for instance ids like todos#1 and as the default persistence key), a setup function, and an optional lifetime. The setup receives a context with defineState, defineActions, and defineEvents, and whatever it returns becomes the store.
import { defineStore } from '@sigx/store';
const useMyStore = defineStore('my-store', (ctx) => {
// ctx.defineState(...), ctx.defineActions(...), ctx.defineEvents(...)
return { /* spread signals, actions, computeds, anything else */ };
}, 'scoped');
defineStore returns a factory function. Calling that factory creates (or resolves, depending on the lifetime) an instance. By convention it is named useXxxStore, but it is just a function — there is no separate useStore export.
This page is the conceptual map: each building block in brief, with a deep guide behind it. If you want the full picture of one topic, follow the link at the end of its section.
State
defineState(obj) turns a plain object into deep reactive, signal-backed state. It returns four things:
const { state, signals, events, patch } = ctx.defineState({ count: 0, name: 'Ada' });
state— the deep reactive object. Read and write properties freely inside the setup and actions; nested objects and arrays are reactive too.signals— spreadable per-key signal views. The keys you return become the store's public state; keys you don't return stay private to the setup.events— one change event per key (events.count,events.name) with(value, prev)callbacks. Lazy: the deep watcher behind a key only runs while someone is subscribed — see Events & Observability.patch— atomic multi-key update; the whole update flushes reactivity once. Accepts a partial object or a mutator function. Errors inside the mutator propagate to the caller.
state.count = 5; // direct assignment
patch({ count: 42, name: 'Grace' }); // atomic multi-key (object form)
patch(s => { s.count++; }); // atomic (mutator form)
Public vs. private state
Only the key signals you spread into the return are part of the store's surface — and of $patch / $events typing on the instance:
const useCounter = defineStore('counter', ({ defineState, defineActions }) => {
const { state, signals } = defineState({ count: 0, internalTicks: 0 });
const actions = defineActions({
increment() { state.count++; state.internalTicks++; },
});
// expose count, keep internalTicks private:
const { count } = signals;
return { count, ...actions };
});
const store = useCounter();
store.count; // ✔ public
// store.internalTicks — not on the store, and not in $patch/$events
How that selection drives the instance types is covered in TypeScript.
Derived / computed values
@sigx/store does not ship its own getter helper. Build derived values with computed and return them directly — on the instance they unwrap to plain read-only values.
import { computed } from 'sigx';
import { defineStore } from '@sigx/store';
const useCartStore = defineStore('cart', ({ defineState }) => {
const { state, signals } = defineState({
items: [
{ name: 'Keyboard', price: 80, qty: 1 },
{ name: 'Mouse', price: 30, qty: 2 },
],
});
const total = computed(() =>
state.items.reduce((sum, item) => sum + item.price * item.qty, 0)
);
return { ...signals, total };
});
const store = useCartStore();
store.total; // plain number — no .value
store.total = 0; // ✗ throws: computed values are read-only
Actions
defineActions(obj) wraps a set of functions. Each returned action is callable with its exact original signature, plus a reactive in-flight flag and three lifecycle event subscribers riding on the function itself:
const actions = ctx.defineActions({
add(a: number, b: number) { return a + b; },
async fetchData() { return await api.load(); },
});
actions.add(3, 7); // => 10
actions.fetchData.pending; // reactive: true while any invocation is in flight
actions.fetchData.onFailure.subscribe((error) => console.error('fetch failed', error));
action.onDispatching.subscribe(fn)— before the action body, with the original args.action.onDispatched.subscribe(fn)— after the action, with(result, ...args); for async actions, after the promise resolves, with the resolved value.action.onFailure.subscribe(fn)— on a sync throw or async rejection, with(error, ...args).
Errors are the caller's to handle — never swallowed: sync actions re-throw, async actions return the original promise. The full story — overlapping invocations, optimistic updates, aborts, race patterns — lives in Actions & Async.
Declare each action as a single function signature (union parameter types instead of overloads); the event subscriber types are inferred from that one signature. Rationale in TypeScript.
Custom events
defineEvents<EventMap>() creates a typed group of topics, auto-namespaced to the store instance (${id}.events) and destroyed with it. Return the ones you want subscribers to reach:
import { defineStore } from '@sigx/store';
type CartEvents = {
checkedOut: { orderId: string };
cleared: void;
};
const useCartStore = defineStore('cart', (ctx) => {
const { state, signals, patch } = ctx.defineState({ items: [] as Item[] });
const events = ctx.defineEvents<CartEvents>();
const actions = ctx.defineActions({
async checkout() {
const orderId = await api.checkout(state.items);
patch({ items: [] });
events.checkedOut.publish({ orderId });
},
});
return { ...signals, ...actions, onCheckedOut: events.checkedOut };
});
useCartStore().onCheckedOut.subscribe(({ orderId }) => { /* typed */ });
Every subscribe() returns a Subscription with unsubscribe(). When to reach for custom events versus per-key state events versus action events — and how devtools discovers all of them — is the subject of Events & Observability.
Lifetime
The third argument to defineStore is the instance lifetime — 'singleton' | 'scoped' | 'transient', defaulting to 'scoped'. It is honored by the core DI layer (core 0.5.0): singletons are shared per app, scoped instances respect defineProvide subtree overrides, transients are created per call. Lifetimes & DI covers each in practice, plus parameterized stores and the SSR contract.
Composables
Because a store is "just" a setup function, reusable behavior is a plain function that takes the context — the built-in persist composable is the canonical example:
import { persist } from '@sigx/store/persist';
const useSettings = defineStore('settings', (ctx) => {
const { state, signals, patch } = ctx.defineState({ theme: 'dark' });
const { hydrated } = persist(ctx, { state, patch });
return { ...signals, hydrated };
}, 'singleton');
Writing your own composables — and when to use the global onStoreCreated plugin hook instead — is covered in Composables & Plugins.
A complete store
Putting state, derived values, and actions together:
import { computed } from 'sigx';
import { defineStore } from '@sigx/store';
const useTodoStore = defineStore('todos', ({ defineState, defineActions }) => {
const { state, signals, patch } = defineState({
todos: [] as { id: number; text: string; done: boolean }[],
nextId: 1,
});
const remaining = computed(() => state.todos.filter(t => !t.done).length);
const actions = defineActions({
add(text: string) {
patch(s => {
s.todos.push({ id: s.nextId, text, done: false });
s.nextId++;
});
},
toggle(id: number) {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
},
clearCompleted() {
state.todos = state.todos.filter(t => !t.done);
},
});
return { ...signals, ...actions, remaining };
}, 'scoped');
For larger stores — multiple slices, stores composed of stores, undo/redo — see Patterns.
Next steps
- Actions & Async —
pending, lifecycle events, and error semantics in depth. - Events & Observability — the event mental model and devtools discovery.
- Lifetimes & DI — lifetimes in practice,
defineProvide, SSR. - Using Stores in Components — reading state in render, sharing instances, cleanup.
- API Reference — full signatures for every export.
