State Management — Overview
@tagna/udiot v1.0.0-rc.1
Architecture Overview
UDIOT's state layer is a composable stack of three major concerns: central state storage (the Store), reactive primitives (Observables and Subjects), and derived views (Selectors). These pieces work together but can be used independently.
┌────────────────────────────────────────────────────────┐
│ Application │
│ dispatch(action) ──► Store ◄── subscribe() │
└─────────────────────┬──────────────────────────────────┘
│ state changes
┌────────────▼───────────────┐
│ Middleware Pipeline │
│ (thunk, logger, custom) │
└────────────┬───────────────┘
│ transforms action
┌────────────▼───────────────┐
│ reduce(state, action)│ (abstract, you implement)
└────────────┬───────────────┘
│ new state
┌────────────▼───────────────┐
│ History Buffer │ (time-travel)
└────────────┬───────────────┘
│
┌────────────▼───────────────┐
│ Selectors (memoized) │
│ Observables / Subjects │
└────────────────────────────┘
The Store Pattern
Store<TState> is an abstract class. You subclass it, provide an initial state, and implement a reduce(state, action) method that describes how each action transforms the state.
Key invariants enforced by the Store:
- Immutability — the state object is deep-frozen after every dispatch. Attempting to mutate state directly throws in strict mode.
- Single source of truth —
getState()always returns the current frozen snapshot. - Predictable updates — the only way to change state is to dispatch a typed action object.
interface Action {
type: string;
payload?: any;
meta?: any;
error?: boolean;
}
The reducer is pure: given the same state and action it must always produce the same next state.
Reactive: Subscriptions
Every Store instance has a subscriber set. Listeners receive three arguments: the new state, the previous state, and the action that caused the change.
type StateListener<TState> = (
state: TState,
prevState: TState,
action: Action
) => void;
const unsubscribe = store.subscribe((state, prev, action) => {
console.log(`${action.type}: count went from ${prev.count} to ${state.count}`);
});
// later…
unsubscribe();
subscribe() returns an Unsubscribe function. Always call it when the listener is no longer needed to avoid memory leaks.
Observable Primitives
UDIOT provides a lightweight Observable implementation with the same conceptual API as RxJS, but without the full dependency.
| Class | Description |
|---|---|
Observable<T> |
Cold — producer runs per subscriber |
Subject<T> |
Hot — multicasts to all current subscribers |
BehaviorSubject<T> |
Hot + replays current value to new subscribers |
ReplaySubject<T> |
Hot + replays last N values to new subscribers |
AsyncSubject<T> |
Emits only the last value on complete() |
The Store exposes its state as an Observable through store.asObservable() and lets you select a slice via store.select(fn).
Selectors: Memoized Computed Views
Selectors are memoized functions over state. They cache their result and only recompute when their inputs change.
const selectCompletedTodos = createSelector(
(state: TodoState) => state.todos,
(todos) => todos.filter(t => t.completed)
);
// Calling selectCompletedTodos(state) twice with the same state returns the
// cached result on the second call — no new array is created.
Selectors compose: the output of one selector can be the input of another.
Time-Travel Debugging
The Store keeps a ring buffer of StateSnapshot<TState> objects capped at maxHistory (default 50). After each dispatch the new state is pushed into the buffer. undo(), redo(), and jumpToSnapshot(index) restore historical states and notify subscribers.
store.undo(); // step back one
store.redo(); // step forward one
store.jumpToSnapshot(0); // jump to the very first recorded state
store.getHistory(); // array of all snapshots
AI-Powered Features
When @AIPowered is applied (or store.enableAI() is called), three AI components are attached:
- StateAnalyzer — scores state structure on each dispatch and surfaces issues.
- AIOptimizer — tracks action latency patterns and emits
OptimizationSuggestionobjects. - AIPredictor — records state transition sequences and detects anomalies.
These features are opt-in and have no effect on stores that do not use them.
Middleware
Middleware intercepts every dispatch() call. The new pipeline format uses an async (context, next) => Promise<void> signature; the legacy (store, action) => Action | void format is also supported for backward compatibility.
Middleware runs before the reducer. It can:
- Transform or replace the action.
- Dispatch additional actions (useful for async operations with
thunkMiddleware). - Log, measure, or cancel actions.
When to Use What
| Situation | Use |
|---|---|
| Global app state shared across many components | Store subclass |
| Derived/computed view of state | createSelector |
| Local component state, no sharing needed | BehaviorSubject |
| Event bus (multiple producers, multiple consumers) | Subject |
| Streaming data (WebSocket, SSE) | Observable |
| State that must survive page refresh | Store + @Persist |
| Async operations (fetch, timers) | thunkMiddleware + createAsyncThunk |