Docs/State & reactivityv1.0.0-rc.1

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 OptimizationSuggestion objects.
  • 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
From the canonical UDIOT public documentation.Try it in the playground ↗