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
| Name | Type | Description |
|---|---|---|
name | string | Unique within the container. |
options.state | () => S | Factory returning initial state. |
options.getters | G | Derived values; each receives state. |
options.actions | A | Methods; this is the whole store. |
options.persist | PersistenceConfig<S> | See Persistence. |
container | StoreContainer | Which 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:
| Member | Description |
|---|---|
$id | The store's registered name. |
$signal | Inside an action: an AbortSignal for that call. |
$patch(partial | mutator) | Apply several changes as one batch. |
$reset() | Restore initial state, as one batch. |
$persist | PersistenceManager, or null when persistence is not configured. |
$hydrated | Promise 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. |
state | The 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
| Condition | Behaviour |
|---|---|
| Duplicate name in the same container | Returns the existing instance |
state is not a function returning an object | Throws |
| Action name collides with a state key or getter | Throws |
| Getter name shadows a state key | Allowed; 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 container | Throws |
Learn More
- defineStore — the accessor form, and the default choice
- Containers — isolation, SSR and testing
- Async Actions —
pending,error,abort() - Persistence API
- Managing Stores