defineStore

defineStore is the entry point for every store in QuantaJS 2.1. It returns a definition — a blueprint you call to get an instance — rather than the instance itself.

import { defineStore } from '@quantajs/core';

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] as Item[] }),
  getters: {
    total: (s) => s.items.reduce((n, i) => n + i.price, 0),
  },
  actions: {
    add(item: Item) {
      this.items.push(item);
    },
  },
});

const cart = useCartStore();
cart.total;      // number — inferred, no generics restated
cart.add(item);  // fully typed

Why a definition and not an instance

A definition holds no state. State only exists once the definition is resolved against a container. That single property is what buys you three things:

  • It is safe at module scope. On a server, a module-scope instance is a process-wide singleton shared by every request. A module-scope definition is not.
  • Types survive the call site. The old useStore<S, G, A>('cart') needed three generics restated at every use, so in practice most consumers got any. Calling a definition infers everything.
  • Resolution is idempotent. Resolving the same name twice in one container returns the same instance, which makes HMR, React StrictMode double-mounts and repeated test setup safe instead of throwing.

Signature

function defineStore<S extends StateTree, G extends GettersTree<S>, A extends ActionsTree>(
  name: string,
  options: {
    state: () => S;
    getters?: G;
    actions?: A;
    persist?: PersistenceConfig<S>;
  },
): StoreDefinition<S, G, A>;

interface StoreDefinition<S, G, A> {
  /** Resolve the store. Omit the container in the browser; always pass one on a server. */
  (container?: StoreContainer): Store<S, G, A>;
  readonly $id: string;
  readonly $options: StoreDefinitionOptions<S, G, A>;
}

Parameters

NameTypeDescription
namestringUnique identifier within a container. Used by DevTools, SSR snapshots and persistence keys.
options.state() => SFactory returning the initial state. Called once per container.
options.gettersGDerived values. Each receives state as its only argument.
options.actionsAMethods. this is the whole store — state, getters and other actions.
options.persistPersistenceConfig<S>Optional. See Persistence.

Returns

A callable StoreDefinition. Call it to resolve the store:

const cart = useCartStore();           // ambient container
const scoped = useCartStore(container); // a specific container

Typing state

Everything is inferred from options, so you never pass generics.

Do not pass explicit generics:

defineStore<CounterState>('counter', …) breaks inference: supplying S fixes G and A at their defaults, so your getters and actions disappear from the resulting type. An interface also fails the StateTree constraint, because interfaces do not get an implicit index signature.

Annotate the state factory instead — same checking, nothing lost:

// ✗ getters and actions vanish from the type
const useCounter = defineStore<CounterState>('counter', options);

// ✓ state is checked, getters and actions still inferred
const useCounter = defineStore('counter', {
  state: (): CounterState => ({ count: 0, name: 'Counter' }),
  getters: { doubled: (s) => s.count * 2 },
  actions: { increment() { this.count++; } },
});

For a nested value, annotate at the property:

state: () => ({
  user: null as User | null,
  items: [] as Item[],
})

Getters

Getters take state and return a derived value. They are cached and only recompute when a dependency they actually read has changed.

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] as Item[], taxRate: 0.0825 }),
  getters: {
    subtotal: (s) => s.items.reduce((n, i) => n + i.price, 0),
    itemCount: (s) => s.items.length,
  },
});

To derive from another getter, read it off the store rather than from state:

const cart = useCartStore();
const withTax = () => cart.subtotal * (1 + cart.taxRate);

Note:

A getter whose name shadows a state key wins on the flattened store as of 2.1. The state value is still reachable at store.state.x. Prefer not to shadow at all — the library warns in development when you do.

Actions

Inside an action, this is the full store, so state, getters and sibling actions are all reachable and all typed.

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] as Item[] }),
  getters: {
    total: (s) => s.items.reduce((n, i) => n + i.price, 0),
  },
  actions: {
    add(item: Item) {
      this.items.push(item);       // state
    },
    logTotal() {
      console.log(this.total);     // getter
    },
    addAndLog(item: Item) {
      this.add(item);              // sibling action
      this.logTotal();
    },
  },
});

Actions do not need to reassign arrays or objects to trigger reactivity. this.items.push(item) is enough — mutating nested state is tracked. (If you carried a this.items = [...this.items] workaround over from an older version, delete it; it costs a copy and buys nothing.)

Every action — synchronous or not — carries a lifecycle surface:

cart.checkout.pending   // boolean, reactive
cart.checkout.error     // Error | null
cart.checkout.abort()   // aborts `this.$signal`

Store instance members

Everything a resolved store exposes beyond your own state, getters and actions:

MemberDescription
$idThe store's registered name.
$signalInside an action: an AbortSignal tied to that call.
$patch(partial)Merge a partial state object in one batch.
$patch(mutator)Run a mutator against state in one batch.
$reset()Restore the initial state, as one batch.
$persistThe PersistenceManager, or null if persistence is not configured.
$hydratedA promise resolving once persisted state has been restored.
$dehydrate()A serialisable snapshot of this store's state.
$hydrate(snapshot)Apply a snapshot to this store.
$destroy()Release effects, watchers and persistence, and remove the store from its container.
subscribe(fn)Coarse "something changed" notification. Returns an unsubscribe function.
stateThe underlying reactive state object.

$patch

Both forms apply as a single batch, so subscribers and effects run once rather than once per field.

cart.$patch({ couponCode: 'SPRING', shipping: 0 });

cart.$patch((state) => {
  state.items.push(item);
  state.couponCode = null;
});

Creating an instance eagerly

createStore is the eager counterpart — it returns the instance directly instead of an accessor. Prefer defineStore in shared modules; reach for createStore in a script or a test where you want the store immediately.

import { createStore, createContainer } from '@quantajs/core';

const container = createContainer();
const cart = createStore('cart', options, container);

See createStore.

Migrating from 2.0

- import { createStore, useStore } from '@quantajs/core';
- export const cartStore = createStore('cart', options);
- const cart = useStore('cart');            // untyped without three generics
+ import { defineStore } from '@quantajs/core';
+ export const useCartStore = defineStore('cart', options);
+ const cart = useCartStore();              // fully typed

Naming the definition useXStore is a convention, not a requirement — it is a plain function, not a React hook, and it is callable outside components.

Learn More