Store — API Reference
@tagna/udiot v1.0.0-rc.1
Abstract Class: Store<TState>
import { Store } from '@tagna/udiot';
abstract class Store<TState = any> {
protected abstract reduce(state: TState, action: Action): TState;
// ...
}
Store must be subclassed. The subclass provides an initial state (passed to the super() constructor) and implements the abstract reduce() method.
Constructor
constructor(initialState: TState, options?: StoreOptions)
StoreOptions
interface StoreOptions {
maxHistory?: number; // Maximum number of state snapshots to keep. Default: 50.
enableDevTools?: boolean; // Connect to Redux DevTools on construction. Default: false.
aiOptimizations?: boolean; // Enable AI analysis on construction. Default: false.
}
The initial state is deep-frozen immediately. An @@INIT snapshot is recorded in the history buffer.
interface CounterState {
count: number;
}
class CounterStore extends Store<CounterState> {
constructor() {
super({ count: 0 }, { maxHistory: 100 });
}
protected reduce(state: CounterState, action: Action): CounterState {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
case 'DECREMENT': return { count: state.count - 1 };
case 'RESET': return { count: 0 };
default: return state;
}
}
}
State Access
getState()
getState(): TState
Returns the current immutable state snapshot. The returned object is deep-frozen; any attempt to mutate it will throw in strict mode.
const state = store.getState();
console.log(state.count); // 0
Dispatch
dispatch(action)
async dispatch(action: Action): Promise<void>
Triggers a state update. The execution order on every dispatch:
- Create a
StateContextwrapping the current store and action. - Run the middleware pipeline (may transform
action, or consume it by not callingnext()). - At the end of the pipeline, call
reduce(currentState, finalAction)to compute the next state. - If the state reference changed, deep-freeze the new state.
- Push a
StateSnapshotto the history buffer. - Notify all subscribers, including the observable from
asObservable()(once per change). - Middleware code after
await next()runs and sees the new state.
dispatch is async because the middleware pipeline is async. Awaiting it guarantees all middleware and listener calls have completed. It rejects when a middleware or the reducer throws, and when something other than an action object reaches the reducer (dispatch functions through thunkMiddleware()).
replaceState(state, type?, options?)
replaceState(state: TState, type?: string, options?: { recordHistory?: boolean }): void
Replaces the whole state without running the reducer, freezes it and notifies
listeners with an action of type (default '@@REPLACE'). Meant for tooling:
persistence, DevTools time travel and imports. Pass recordHistory: false to
leave the history untouched.
await store.dispatch({ type: 'INCREMENT' });
await store.dispatch({ type: 'RESET', payload: null });
Action Interface
interface Action {
type: string; // Required. Identifies the action.
payload?: any; // Optional data for the reducer.
meta?: any; // Optional metadata (timestamps, request IDs, etc.)
error?: boolean; // True when this action carries an error payload.
}
Subscription
subscribe(listener)
subscribe(listener: StateListener<TState>): Unsubscribe
Registers a listener that is called after every successful dispatch that changes state. Returns an Unsubscribe function.
type StateListener<TState> = (
state: TState,
prevState: TState,
action: Action
) => void;
type Unsubscribe = () => void;
const stop = store.subscribe((state, prev, action) => {
console.log(`[${action.type}] count: ${prev.count} → ${state.count}`);
});
// Remove listener when done
stop();
Time-Travel
undo()
undo(): void
Reverts to the previous snapshot in history. No-op if already at the oldest snapshot. Notifies subscribers with action type '@@UNDO'.
redo()
redo(): void
Re-applies the next snapshot in history. No-op if already at the most recent snapshot. Notifies subscribers with '@@REDO'.
jumpToSnapshot(index)
jumpToSnapshot(index: number): void
Jumps directly to the snapshot at position index in the history array. Out-of-range indices are silently ignored. Notifies subscribers with '@@JUMP'.
getHistory()
getHistory(): StateSnapshot<TState>[]
Returns a copy of all recorded snapshots. The array length is at most maxHistory.
StateSnapshot<TState>
interface StateSnapshot<TState> {
state: TState; // Deep clone of state at this point
timestamp: number; // Unix milliseconds
action: string; // The action type that produced this snapshot
}
getHistoryIndex()
getHistoryIndex(): number
Returns the index of the currently active snapshot within the history buffer.
canUndo() / canRedo()
canUndo(): boolean
canRedo(): boolean
Convenience checks for time-travel controls.
clearHistory()
clearHistory(): void
Discards all history and records a fresh @@CLEAR_HISTORY snapshot of the current state.
Middleware
use(middleware)
use(middleware: Middleware | PipelineMiddleware<StateContext<TState>>): this
Adds middleware to the dispatch pipeline. Supports both the new pipeline format and the legacy two-argument format for backward compatibility. Returns this for chaining.
New pipeline format (recommended):
store.use({
name: 'logger',
handler: async (ctx, next) => {
console.log('before:', ctx.action.type);
await next();
console.log('after:', ctx.action.type);
}
});
Legacy format:
store.use((store, action) => {
console.log('action:', action.type);
return action;
});
Observable Integration
asObservable()
asObservable(): Observable<TState>
Returns the store's state as a BehaviorSubject-backed Observable<TState>. New subscribers receive the current state immediately. Only one BehaviorSubject is created per store instance.
select(selector)
select<K>(selector: (state: TState) => K): Observable<K>
Returns an Observable that emits the projected value every time the state changes. Uses .map() on the underlying BehaviorSubject.
const count$ = store.select(s => s.count);
count$.subscribe(n => console.log('count:', n));
selectMemoized(selector)
selectMemoized<K>(selector: (state: TState) => K): Observable<K>
Like select() but wraps the selector with createSelector() memoization before mapping. Use this for computationally expensive derived values.
Persistence
enablePersistence(options)
enablePersistence(options: PersistenceOptions<TState>): void
Wires a StatePersister to the store. State is loaded asynchronously on call and saved (debounced) on every subsequent state change. See persistence.md for PersistenceOptions.
Other Methods
getListenerCount()
getListenerCount(): number
Returns the number of active subscribers.
getPipeline()
getPipeline(): MiddlewarePipeline<StateContext<TState>>
Returns the internal middleware pipeline for inspection or testing.
createStore() Function
For simple use cases that do not need a subclass:
function createStore<TState>(
initialState: TState,
reducer: (state: TState, action: Action) => TState,
options?: StoreOptions
): Store<TState>
const store = createStore(
{ count: 0 },
(state, action) => {
switch (action.type) {
case 'INC': return { count: state.count + 1 };
default: return state;
}
}
);
Complete Example: CounterStore
import { Store, type StoreAction as Action } from '@tagna/udiot';
import { loggerMiddleware } from '@tagna/udiot';
interface CounterState {
count: number;
step: number;
}
class CounterStore extends Store<CounterState> {
constructor() {
super({ count: 0, step: 1 });
// Add logger middleware
this.use(loggerMiddleware({ timestamp: true }));
}
protected reduce(state: CounterState, action: Action): CounterState {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step };
case 'DECREMENT':
return { ...state, count: state.count - state.step };
case 'SET_STEP':
return { ...state, step: action.payload as number };
case 'RESET':
return { count: 0, step: 1 };
default:
return state;
}
}
}
// Usage
const store = new CounterStore();
const unsubscribe = store.subscribe((state, prev, action) => {
console.log(`[${action.type}] ${prev.count} → ${state.count}`);
});
await store.dispatch({ type: 'INCREMENT' }); // 0 → 1
await store.dispatch({ type: 'SET_STEP', payload: 5 });
await store.dispatch({ type: 'INCREMENT' }); // 1 → 6
await store.dispatch({ type: 'DECREMENT' }); // 6 → 1
store.undo(); // back to 6
store.redo(); // forward to 1
console.log(store.getHistory().length); // 5 snapshots (@@INIT + 4 dispatches)
unsubscribe();
Pitfalls
Dispatching inside a subscriber (infinite loop)
A subscriber that calls dispatch() will trigger all subscribers again, which may call dispatch() again. Guard against this with a condition check:
store.subscribe((state, prev, action) => {
if (action.type === 'SET_STEP') return; // avoid recursion
if (state.count > 100) {
store.dispatch({ type: 'SET_STEP', payload: 10 });
}
});
Not unsubscribing (memory leak)
subscribe() adds a function to an internal Set. If the subscribing object is long-lived but the store is not, the subscriber keeps the object alive. Always call the returned unsubscribe() function when the subscriber's lifecycle ends.
class MyComponent {
private unsubscribe!: () => void;
onMount(): void {
this.unsubscribe = store.subscribe(state => this.render(state));
}
onUnmount(): void {
this.unsubscribe();
}
}