Defining a Store
Everything in @sigx/store starts with defineStore. It takes a name (used as a debug/instance-name prefix) and a setup function. The setup receives a context with defineState and defineActions, and returns an object describing the store: state, get, actions, events, and an optional name.
import { defineStore } from '@sigx/store';
const useStore = defineStore('my-store', (ctx) => {
// ctx.defineState(...), ctx.defineActions(...)
return { /* state, get, actions, events */ };
});
defineStore returns a factory function. Calling that factory creates an instance. By convention it is named useXxxStore, but it is just a function — there is no separate useStore export.
State
defineState(obj) turns a plain object into reactive, signal-backed state. It returns three things:
const { state, events, mutate } = ctx.defineState({ count: 0, name: 'Ada' });
state— the reactive object. Read and write properties directly; deep watching is on, so nested object changes also trigger events.events— oneonMutated{Key}subscriber per property (the key is capitalized):onMutatedCount,onMutatedName.mutate— one helper per property that accepts a value or an updater function.
state.count = 5; // direct assignment
mutate.count(42); // set via helper
mutate.count(prev => prev + 1); // functional update (errors in the updater are caught and logged)
events.onMutatedCount.subscribe(value => {
console.log('count changed to', value);
});
Both direct assignment and mutate fire the matching onMutated{Key} event with the new value.
Derived / computed getters
@sigx/store does not ship its own getter helper. Build derived values with computed from sigx and expose them under the get field of your setup return. Computed values are lazy and cached, and they track the signal-backed state they read.
import { computed } from 'sigx';
import { defineStore } from '@sigx/store';
const useCartStore = defineStore('cart', ({ defineState }) => {
const { state, mutate } = 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)
);
const itemCount = computed(() =>
state.items.reduce((sum, item) => sum + item.qty, 0)
);
return {
state,
get: { total, itemCount },
};
});
Read derived values via .value (the computed convention): store.get.total.value.
Actions
defineActions(obj) wraps a set of functions. The returned object keeps your original callable actions and adds three event namespaces — one entry per action.
const actions = ctx.defineActions({
add: (a: number, b: number) => a + b,
fetchData: async () => 'data',
});
actions.add(3, 7); // => 10
Each action gains lifecycle events:
actions.onDispatching.<action>.subscribe(fn)— runs before the action body, with the original args.actions.onDispatched.<action>.subscribe(fn)— runs after the action, with(result, ...args). For async actions, this fires after the returned promise resolves.actions.onFailure.<action>.subscribe(fn)— runs if the action throws, with(failureReason, ...args).
actions.onDispatching.add.subscribe((a, b) => {
console.log('about to add', a, b);
});
actions.onDispatched.add.subscribe((result, a, b) => {
console.log('added', a, b, '=>', result);
});
actions.onFailure.add.subscribe((error, a, b) => {
console.error('add failed', error);
});
Errors thrown inside an action are caught: onFailure fires, the error is logged via console.error, and the call returns undefined (the error is not re-thrown).
For sync actions the order is: onDispatching → action body → onDispatched. For async actions, onDispatched waits for the promise to resolve.
Custom events
Beyond the automatic onMutated{Key} events, you can expose your own topic-based events under the events field of the setup return. Every subscriber returns a Subscription with unsubscribe().
A complete store
Putting state, getters, and actions together:
import { computed } from 'sigx';
import { defineStore } from '@sigx/store';
const useTodoStore = defineStore('todos', ({ defineState, defineActions }) => {
const { state, mutate } = 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) {
const id = state.nextId;
mutate.todos(prev => [...prev, { id, text, done: false }]);
mutate.nextId(prev => prev + 1);
},
toggle(id: number) {
mutate.todos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
},
clearCompleted() {
mutate.todos(prev => prev.filter(t => !t.done));
},
});
return {
state,
get: { remaining },
actions,
};
});
Next steps
- Using Stores in Components — reading state in render, sharing instances, lifetimes, and cleanup.
- API Reference — full signatures for every export.
