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.

TypeScript
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:

TypeScript
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.
TypeScript
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:

TypeScript
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.

TSX
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TSX
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#