API: createStore
The createStore function is the foundation of SoulState. It creates a store instance that holds your application state and provides methods to read and update it.
createStore
Creates a new store instance. It can take either an initial state object or a "creator" function.
Signature
function createStore<T extends State>(
creator: StateCreator<T> | T
): StoreApi<T>;
creator: The initial state object or a Zustand-style creator function.- Returns: A
StoreApi<T>instance.
Using an Initial State Object
import { createStore } from 'soulstate';
export const counterStore = createStore({
count: 0
});
Using a Creator Function (Zustand style)
The creator function receives setState, getState, and api as arguments.
import { createStore } from 'soulstate';
export const counterStore = createStore((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
getDouble: () => get().count * 2
}));
StoreApi<T> Interface
createStore returns a StoreApi object with the following methods:
interface StoreApi<T extends State> {
getState: () => T;
getVersion: () => number;
setState: (
updater: PartialState<T>,
replace?: boolean,
sync?: boolean
) => void;
subscribe: <S>(
selector: (state: T) => S,
listener: (state: S, prevState: S) => void,
options?: { equalityFn?: (a: S, b: S) => boolean }
) => () => void;
computed: {
<S>(selector: (state: T) => S, name?: string): Computed<S>;
<S extends any[], R>(
...args: [
...{ [K in keyof S]: ((state: T) => S[K]) | Computed<S[K]> },
(...vals: S) => R
]
): Computed<R>;
};
beginTransaction: () => void;
commitTransaction: () => void;
rollbackTransaction: () => void;
enableInstrumentation: (options?: RuntimeInstrumentation) => void;
getMetrics: () => MetricsSnapshot | null;
destroy: () => void;
}
store.getState()
Returns the current state of the store.
Signature
getState: () => T
Behavior
- Returns the current state object.
- If a transaction is active, returns the buffered state from the transaction engine.
- Issues a dev-mode warning if the store has been destroyed.
Example
const state = store.getState();
console.log(state.count); // 0
store.getVersion()
Returns the current version number of the state.
Signature
getVersion: () => number
Behavior
- Returns a monotonically increasing integer.
- Incremented by 1 each time
setStateapplies a non-noop update. - Useful for avoiding unnecessary re-computation in derived selectors.
Example
const v1 = store.getVersion(); // 0
store.setState({ count: 1 });
const v2 = store.getVersion(); // 1
v2 > v1; // true
store.setState()
Updates the store's state.
Signature
setState: (
updater: Partial<T> | ((state: T) => Partial<T> | T),
replace?: boolean,
sync?: boolean
) => void
Parameters
updater: Partial state object or updater function.replace(optional): Iftrue, replaces the entire state instead of merging.sync(optional): Iftrue, bypasses the microtask scheduler and propagates immediately.
Behavior
- Merges partial state into current state by default.
- If the updater produces values identical to the current state (per
Object.is), the update is a no-op. - Multiple synchronous
setStatecalls are batched into a single propagation cycle.
Example
// Object form
store.setState({ count: 5 });
// Updater function form
store.setState((state) => ({ count: state.count + 1 }));
// Replace entire state
store.setState({ count: 0, user: null }, true);
// Synchronous propagation (bypass batching)
store.setState({ count: 1 }, false, true);
store.subscribe()
Subscribes to state changes with granular dependency tracking.
Signature
subscribe: <S>(
selector: (state: T) => S,
listener: (state: S, prevState: S) => void,
options?: { equalityFn?: (a: S, b: S) => boolean }
) => () => void
Parameters
selector: A function to pick data from the state. Automatically tracked for dependency changes.listener: Called whenever the selected value changes.options: OptionalequalityFnfor custom comparison (defaults toObject.is).
Returns
A teardown function that unregisters the listener.
Example
const unsub = store.subscribe(
(s) => s.count,
(count, prevCount) => {
console.log(`count: ${prevCount} → ${count}`);
}
);
// Later:
unsub();
store.computed()
Creates a derived, memoized state node that is part of the reactive graph.
Signature
// Single selector
computed<S>(selector: (state: T) => S, name?: string): Computed<S>;
// Multi-dependency
computed<S extends any[], R>(
...deps: [...((state: T) => S[K])[], (...vals: S) => R]
): Computed<R>;
Parameters
- Single selector form: A selector function and an optional name (string or symbol).
- Multi-dependency form: An array of selector functions or
Computedobjects, followed by a combiner function as the last argument.
Returns
A Computed<S> object (see below).
Example
// Single selector
const doubled = store.computed(
(s) => s.count * 2,
'doubled'
);
console.log(doubled.value); // 2 (if count is 1)
// Multi-dependency
const area = store.computed(
(s) => s.width,
(s) => s.height,
(w, h) => w * h
);
console.log(area.value); // 50 (if width=5, height=10)
Computed<T> Interface
Returned by store.computed(). Represents a memoized derived value in the reactive graph.
interface Computed<T> {
readonly value: T;
readonly name?: string | symbol;
destroy(): void;
}
Properties
| Property | Type | Description |
|---|---|---|
value | T | Lazily evaluated. Recomputes only when dependencies change. |
name | string | symbol | Optional identifier passed during creation. |
destroy() | () => void | Removes the computed node from the reactive graph and cleans up dependencies. |
Lazy Evaluation
The value getter triggers recomputation only when the node is marked dirty. Reading .value during a tracking context (e.g., inside another computed or subscribe selector) registers the current node as a dependency.
Example
const sum = store.computed((s) => s.a + s.b, 'sum');
console.log(sum.value); // Lazily computed
// Subscribe to computed value changes
store.subscribe(
(s) => sum.value,
(val) => console.log('sum changed:', val)
);
// Clean up when no longer needed
sum.destroy();
store.beginTransaction()
Begins a transaction. Updates within a transaction are buffered until committed.
Signature
beginTransaction: () => void
Behavior
- While a transaction is active,
setStatecalls are buffered. getState()returns the buffered state (including pending changes).- Supports nested transactions (depth counter).
Example
store.beginTransaction();
store.setState({ a: 1 });
store.setState({ b: 2 });
store.commitTransaction(); // Single propagation cycle
store.commitTransaction()
Commits the current transaction, applying all buffered updates in a single propagation cycle.
store.rollbackTransaction()
Rolls back the current transaction, discarding all buffered updates. The state reverts to what it was before beginTransaction() was called.
Example
store.beginTransaction();
store.setState({ count: 999 });
store.rollbackTransaction();
store.getState().count; // Unchanged — rollback discarded the update
store.enableInstrumentation()
Enables runtime profiling and diagnostics.
Signature
enableInstrumentation: (options?: RuntimeInstrumentation) => void
interface RuntimeInstrumentation {
onFlush?: (duration: number, changedKeys: Set<string | symbol>) => void;
onSelectorRun?: (name: string | symbol, duration: number) => void;
onInvalidate?: (key: string | symbol) => void;
onRender?: (count: number) => void;
}
Parameters
| Callback | Description |
|---|---|
onFlush | Called after each flush cycle with the duration (ms) and the set of changed keys. |
onSelectorRun | Called after each selector execution with its name and duration. |
onInvalidate | Called when a computed node is marked dirty. |
onRender | Called with the render count. |
Example
store.enableInstrumentation({
onFlush: (duration, keys) => {
console.log(`Flush: ${duration.toFixed(3)}ms for`, keys);
},
onSelectorRun: (name, duration) => {
console.log(`Selector ${String(name)}: ${duration.toFixed(3)}ms`);
}
});
store.getMetrics()
Returns a snapshot of the runtime performance metrics.
Signature
getMetrics: () => MetricsSnapshot | null
interface MetricsSnapshot {
flushCount: number;
selectorRunCount: number;
invalidationCount: number;
averageFlushDuration: number;
totalFlushDuration: number;
lastFlushDuration: number;
}
Returns
MetricsSnapshot if instrumentation is enabled, otherwise null.
Example
store.enableInstrumentation({});
store.setState({ count: 1 });
const metrics = store.getMetrics();
console.log('Flush count:', metrics.flushCount);
console.log('Avg flush:', metrics.averageFlushDuration, 'ms');
store.destroy()
Destroys the store and removes all listeners, computed nodes, and graph edges.
Signature
destroy: () => void
Behavior
- Clears all subscriptions, computed nodes, and graph edges.
- After destruction:
setState()throws[SoulState] Cannot update state on a destroyed storesubscribe()throws[SoulState] Cannot subscribe to a destroyed storebeginTransaction()throws[SoulState] Cannot begin transaction on a destroyed storecomputed()throws[SoulState] Cannot create computed on a destroyed storegetState()returns the last state and issues a dev-mode warning.
Example
const store = createStore({ count: 0 });
// ... use the store ...
store.destroy();
// Store is now cleaned up
Irreversible
destroy() is irreversible. You cannot reuse a destroyed store instance.