createStore

createStore builds a store instance immediately, in a container.

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

const counter = createStore('counter', {
  state: () => ({ count: 0 }),
  getters: { doubled: (s) => s.count * 2 },
  actions: { increment() { this.count++; } },
});

counter.count;       // 0
counter.increment();
counter.doubled;     // 2

Prefer defineStore in shared modules:

defineStore returns an accessor instead of an instance. Because a definition holds no state, it is safe at module scope even on a server, where a module-scope instance is a process-wide singleton shared by every request. Reach for createStore in a script or a test where you want the instance right now, and in library code where you control the container.

Signature

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

Parameters

NameTypeDescription
namestringUnique within the container.
options.state() => SFactory returning initial state.
options.gettersGDerived values; each receives state.
options.actionsAMethods; this is the whole store.
options.persistPersistenceConfig<S>See Persistence.
containerStoreContainerWhich container to create in. Defaults to the ambient one. Always pass one on a server.

Returns

A Store<S, G, A> with flattened access to state, getters and actions.

Idempotent per container

Creating the same name twice in one container returns the existing instance rather than throwing. That is what makes hot-module replacement, React StrictMode double-mounts and repeated test setup safe:

const a = createStore('counter', options);
const b = createStore('counter', options);
a === b; // true

In 2.0.0 the second call threw, which is why getOrCreateStore existed. It has been removed — this is the behaviour now.

Store members

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

MemberDescription
$idThe store's registered name.
$signalInside an action: an AbortSignal for that call.
$patch(partial | mutator)Apply several changes as one batch.
$reset()Restore initial state, as one batch.
$persistPersistenceManager, or null when persistence is not configured.
$hydratedPromise resolving once persisted state has been restored.
$dehydrate()Serialisable snapshot of this store's state.
$hydrate(snapshot)Apply a snapshot to this store.
$destroy()Release effects, watchers and persistence; remove from the container.
subscribe(fn)Coarse change notification. Returns an unsubscribe function.
stateThe underlying reactive state object.

subscribe

const unsubscribe = counter.subscribe((snapshot) => {
  console.log('changed:', snapshot);
});

unsubscribe();

subscribe is coarse by design — it fires on "something changed", not on a specific key. It also fires when an action's pending or error flips, so a loading indicator built on subscribe works even for an action that writes no state. For fine-grained reactions, use watch or, in React, useQuantaValue.

$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;
});

$reset

counter.count = 10;
counter.$reset(); // back to { count: 0 }, as one batch

$destroy

Removes the store from its container and releases everything it owns — effects, watchers, persistence subscriptions.

const temp = createStore('temporary', {
  state: () => ({ data: [] }),
  persist: { adapter: new LocalStorageAdapter('temp') },
});

temp.$destroy();

Using a store after $destroy() is not supported. To tear down a whole group of stores, dispose their container instead — one call covers all of them.

TypeScript

Everything is inferred from options. You do not supply generics:

const counter = createStore('counter', {
  state: () => ({ count: 0, name: 'Counter' }),
  getters: {
    doubled: (s) => s.count * 2,
    greeting: (s) => `Hello ${s.name}!`,
  },
  actions: {
    increment() { this.count++; },
    setName(name: string) { this.name = name; },
  },
});

counter.doubled;   // number
counter.greeting;  // string
counter.setName('x');
counter.setName(1); // ✗ Argument of type 'number' is not assignable to 'string'

Do not pass explicit generics:

createStore<CounterState>('counter', …) breaks inference. Supplying S fixes G and A at their defaults, so your getters and actions vanish from the resulting type:

const counter = createStore<CounterState>('counter', options);
counter.doubled;   // ✗ Property 'doubled' does not exist on type 'Store<CounterState, {}, {}>'
counter.increment; // ✗ same

An interface also fails the StateTree constraint, because interfaces do not get an implicit index signature. Annotate the state factory instead — you get the same checking with none of the loss:

const counter = createStore('counter', {
  state: (): CounterState => ({ count: 0, name: 'Counter' }),
  getters: { doubled: (s) => s.count * 2 },
  actions: { increment() { this.count++; } },
});

Reference an interface for a nested value the same way — through the state factory:

state: () => ({ user: null as User | null })

Working in a container

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

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

container.dispose(); // destroys every store created in it

See Containers.

Nested state is tracked

Mutating nested state is enough — you never need to reassign an array or object to make a change visible:

cart.items.push(item);              // ✅ tracked
cart.user.address.city = 'Berlin';  // ✅ tracked
cart.items = [...cart.items];       // ❌ unnecessary copy, no benefit

Adding a property also invalidates Object.keys / for...in / spread dependents, and array mutators trigger once per call rather than once per internal write.

Batching

Several writes in one turn already coalesce into one notification when they happen inside an action or a $patch. For writes outside those, use batchEffects:

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

const result = batchEffects(() => {
  store.count = 10;
  store.name = 'Updated';
  store.items.push(newItem);
  return store.count;
});
// Effects run once, after the batch. `batchEffects` returns the callback's value.

Errors

ConditionBehaviour
Duplicate name in the same containerReturns the existing instance
state is not a function returning an objectThrows
Action name collides with a state key or getterThrows
Getter name shadows a state keyAllowed; the getter wins on the flat store, and a warning is logged in development. The state value stays reachable at store.state.x
Resolving in a disposed containerThrows

Learn More