Skip to main content

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 setState applies 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

  1. updater: Partial state object or updater function.
  2. replace (optional): If true, replaces the entire state instead of merging.
  3. sync (optional): If true, 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 setState calls 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

  1. selector: A function to pick data from the state. Automatically tracked for dependency changes.
  2. listener: Called whenever the selected value changes.
  3. options: Optional equalityFn for custom comparison (defaults to Object.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 Computed objects, 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

PropertyTypeDescription
valueTLazily evaluated. Recomputes only when dependencies change.
namestring | symbolOptional identifier passed during creation.
destroy()() => voidRemoves 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, setState calls 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

CallbackDescription
onFlushCalled after each flush cycle with the duration (ms) and the set of changed keys.
onSelectorRunCalled after each selector execution with its name and duration.
onInvalidateCalled when a computed node is marked dirty.
onRenderCalled 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 store
    • subscribe() throws [SoulState] Cannot subscribe to a destroyed store
    • beginTransaction() throws [SoulState] Cannot begin transaction on a destroyed store
    • computed() throws [SoulState] Cannot create computed on a destroyed store
    • getState() 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.