State Decorators — Reference
@tagna/udiot v1.0.0-rc.1
All store decorators require
emitDecoratorMetadata: trueandexperimentalDecorators: trueintsconfig.json.
@ReactiveStore(options?)
Target: class (must extend Store<TState>)
Registers the store class in StoreRegistry and wraps the constructor to merge decorator options with any options passed at construction time. Each constructed instance is also registered in StoreRegistry so it can be looked up by name at runtime.
function ReactiveStore<TState = any>(options?: StoreDecoratorOptions): ClassDecorator
StoreDecoratorOptions
interface StoreDecoratorOptions {
name?: string; // Registry name. Defaults to class name.
aiTracking?: boolean; // Enable AI optimization tracking.
trackHistory?: boolean; // Enable time-travel history.
devTools?: boolean; // Connect to Redux DevTools on construction.
maxHistory?: number; // Override default history buffer size.
}
import { ReactiveStore, Store, type StoreAction as Action } from '@tagna/udiot';
@ReactiveStore({ name: 'CounterStore', trackHistory: true })
class CounterStore extends Store<{ count: number }> {
constructor() {
super({ count: 0 });
}
protected reduce(state: { count: number }, action: Action) {
return action.type === 'INC' ? { count: state.count + 1 } : state;
}
}
@Mutation(options?)
Target: method on a Store subclass
Marks a synchronous method as a state mutation. The decorator:
- Stores
MutationMetadataon the class. - Optionally validates that the method dispatches an action rather than mutating state directly (development only, when
validate: true). - Optionally emits mutation events to the AI tracking system (
aiTracking: true).
Mutations should be synchronous and must call this.dispatch() to change state. Use @Action for async operations.
function Mutation(options?: MutationOptions): MethodDecorator
MutationOptions
interface MutationOptions {
name?: string; // Override the registered name. Defaults to method name.
validate?: boolean; // Enable development-time validation.
aiTracking?: boolean; // Send mutation events to AI tracker.
description?: string; // Documentation string.
}
class TodoStore extends Store<TodoState> {
@Mutation({ aiTracking: true })
addTodo(text: string): void {
this.dispatch({
type: 'ADD_TODO',
payload: { id: crypto.randomUUID(), text, completed: false }
});
}
@Mutation()
toggleTodo(id: string): void {
this.dispatch({ type: 'TOGGLE_TODO', payload: id });
}
}
@Action(options?)
Target: method on a Store subclass
Marks an async method as an action. On each call the decorator automatically dispatches lifecycle actions:
| Action type | When dispatched |
|---|---|
${actionName}_PENDING |
Before the method body executes |
${actionName}_FULFILLED |
After the method resolves successfully |
${actionName}_REJECTED |
After the method throws |
Additional capabilities: debounce, throttle, retry with exponential backoff, and result caching. Debounce and throttle are tracked per store instance; when calls are debounced, only the last one runs and every caller's promise settles with its result. Throttled calls resolve to undefined without running. Cached results are keyed by class, action name and arguments. Lifecycle actions are awaited; a reducer that throws on one is logged and does not change the method's own result.
function Action(options?: ActionOptions): MethodDecorator
ActionOptions
interface ActionOptions {
name?: string; // Override lifecycle action prefix.
debounce?: number; // Debounce delay in ms.
throttle?: number; // Throttle interval in ms.
retry?: number; // Number of retry attempts on failure.
retryDelay?: number; // Base retry delay in ms (uses exponential backoff).
cache?: boolean; // Cache the result by arguments.
cacheTTL?: number; // Cache time-to-live in ms. Default: 60000.
aiTracking?: boolean; // Send action events to AI tracker.
description?: string; // Documentation string.
}
class UserStore extends Store<UserState> {
@Action({ retry: 2, cache: true, cacheTTL: 30_000 })
async fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
}
// In the reducer:
// 'fetchUser_PENDING' → set loading: true
// 'fetchUser_FULFILLED' → set user: action.payload
// 'fetchUser_REJECTED' → set error: action.payload.message
@Selector(options?)
Target: method on a Store subclass
Converts a method into a memoized selector. The method body acts as the combiner function. On each call a MemoizedSelector is created (once, lazily, per instance) and its result is returned.
function Selector(options?: SelectorDecoratorOptions): MethodDecorator
SelectorDecoratorOptions
interface SelectorDecoratorOptions extends SelectorOptions {
name?: string;
equalityFn?: (a: any, b: any) => boolean;
aiTracking?: boolean;
deps?: string[]; // Names of other @Selector methods to use as inputs
}
When deps is provided the decorator composes the dependency selectors — the dependency results are passed as arguments to the method instead of the full state.
class TodoStore extends Store<TodoState> {
@Selector({ name: 'todos' })
allTodos(): Todo[] {
return this.getState().todos;
}
@Selector({ deps: ['allTodos'] })
completedTodos(todos: Todo[]): Todo[] {
return todos.filter(t => t.completed);
}
@Selector({ deps: ['completedTodos'] })
completedCount(completed: Todo[]): number {
return completed.length;
}
}
const store = new TodoStore();
store.completedTodos(); // memoized, uses allTodos() as input
@Computed is an alias for @Selector.
@Computed()
Target: getter on a component class (when used in src/decorators/state.ts context)
When applied to a Store method, @Computed is an alias for @Selector (see above).
When applied to a component getter (from src/decorators/state.ts), it installs a cache that invalidates when the component's state changes.
@Component({ selector: 'shopping-cart' })
class ShoppingCart extends BaseComponent {
@State()
items: CartItem[] = [];
@Computed()
get totalPrice(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
@State(initialValue?)
Target: property on a component class (from src/decorators/state.ts)
Marks a property as reactive component state. Installs a getter/setter pair that:
- Stores the value in a private Symbol key.
- On set — if the value changed, calls
this.onStateChange()andthis.scheduleRender()if the component is mounted.
function State(): PropertyDecorator
@Component({ selector: 'counter' })
class CounterComponent extends BaseComponent {
@State()
count: number = 0;
increment(): void {
this.count++; // triggers re-render
}
}
@Persist(options)
Target: class (must extend Store<TState>)
Wires a StatePersister to the store at construction time. See persistence.md for the full PersistDecoratorOptions reference.
function Persist<TState = any>(options?: PersistDecoratorOptions<TState>): ClassDecorator
@ReactiveStore({ name: 'PrefsStore' })
@Persist({
storage: 'localStorage',
key: 'user-prefs',
version: 1,
whitelist: ['theme', 'language']
})
class PrefsStore extends Store<PrefsState> {
constructor() { super({ theme: 'light', language: 'en' }); }
protected reduce(state: PrefsState, action: Action): PrefsState { /* ... */ return state; }
}
@AIPowered(options?)
Target: class (must extend Store<TState>)
Enables AI analysis, optimization suggestions, and anomaly detection for the store. Wraps the constructor to call setupAIFeatures() asynchronously after construction.
function AIPowered(options?: AIPoweredOptions): ClassDecorator
AIPoweredOptions
interface AIPoweredOptions {
analyzer?: StateAnalyzerConfig; // Configuration for StateAnalyzer
optimizer?: OptimizerConfig; // Configuration for AIOptimizer
predictor?: PredictorConfig; // Configuration for AIPredictor
autoAnalyze?: boolean; // Analyze on every dispatch. Default: true.
autoOptimize?: boolean; // Emit suggestions on every dispatch. Default: true.
logInsights?: boolean; // Log low-score analysis to console. Default: false.
onSuggestion?: (s: OptimizationSuggestion) => void;
onAnomaly?: (a: Anomaly) => void;
}
@ReactiveStore({ name: 'SmartStore' })
@AIPowered({
autoAnalyze: true,
logInsights: true,
onSuggestion: (s) => console.warn('[AI]', s.message)
})
class SmartStore extends Store<AppState> {
constructor() { super(initialState); }
protected reduce(state: AppState, action: Action): AppState { /* ... */ return state; }
}
Access AI components at runtime:
const store = new SmartStore();
store.getAnalyzer(); // StateAnalyzer
store.getOptimizer(); // AIOptimizer
store.getPredictor(); // AIPredictor
@EnableDevTools(options?)
Target: class (must extend Store<TState>)
Connects the store to the Redux DevTools browser extension and attaches a StateInspector. Wraps the constructor to call setupDevTools() asynchronously after construction.
function EnableDevTools(options?: EnableDevToolsOptions): ClassDecorator
EnableDevToolsOptions
interface EnableDevToolsOptions {
devTools?: DevToolsConfig; // Redux DevTools configuration
inspector?: InspectorConfig; // StateInspector configuration
consoleLogger?: boolean; // Log to console as well. Default: false.
autoConnect?: boolean; // Connect on construction. Default: true.
}
@ReactiveStore({ name: 'AppStore' })
@EnableDevTools({ consoleLogger: true })
class AppStore extends Store<AppState> {
constructor() { super(initialState); }
protected reduce(state: AppState, action: Action): AppState { /* ... */ return state; }
}
Complete Example: TodoStore
import { Store, type StoreAction as Action } from '@tagna/udiot';
import { ReactiveStore, Mutation, Action as ActionDecorator, Selector, Persist, EnableDevTools } from '@tagna/udiot';
interface Todo { id: string; text: string; completed: boolean; }
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
loading: boolean;
}
const initial: TodoState = { todos: [], filter: 'all', loading: false };
@ReactiveStore({ name: 'TodoStore', trackHistory: true })
@Persist<TodoState>({
storage: 'localStorage',
key: 'todo-store',
version: 1,
whitelist: ['todos', 'filter']
})
@EnableDevTools({ consoleLogger: false })
class TodoStore extends Store<TodoState> {
constructor() {
super(initial, { maxHistory: 50 });
}
protected reduce(state: TodoState, action: Action): TodoState {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload as Todo] };
case 'REMOVE_TODO':
return { ...state, todos: state.todos.filter(t => t.id !== action.payload) };
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(t =>
t.id === action.payload ? { ...t, completed: !t.completed } : t
)
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'fetchTodos_PENDING':
return { ...state, loading: true };
case 'fetchTodos_FULFILLED':
return { ...state, loading: false, todos: action.payload as Todo[] };
case 'fetchTodos_REJECTED':
return { ...state, loading: false };
default:
return state;
}
}
// --- Mutations ---
@Mutation()
addTodo(text: string): void {
this.dispatch({
type: 'ADD_TODO',
payload: { id: crypto.randomUUID(), text, completed: false }
});
}
@Mutation()
removeTodo(id: string): void {
this.dispatch({ type: 'REMOVE_TODO', payload: id });
}
@Mutation()
toggleTodo(id: string): void {
this.dispatch({ type: 'TOGGLE_TODO', payload: id });
}
// --- Async Action ---
@ActionDecorator({ retry: 2 })
async fetchTodos(): Promise<Todo[]> {
const res = await fetch('/api/todos');
return res.json();
}
// --- Selectors ---
@Selector()
completedTodos(): Todo[] {
return this.getState().todos.filter(t => t.completed);
}
@Selector({ deps: ['completedTodos'] })
completedCount(completed: Todo[]): number {
return completed.length;
}
@Selector()
activeTodos(): Todo[] {
return this.getState().todos.filter(t => !t.completed);
}
}
// --- Usage ---
const store = new TodoStore();
store.addTodo('Buy milk');
store.addTodo('Walk the dog');
store.toggleTodo(store.getState().todos[0].id);
console.log(store.completedCount()); // 1
console.log(store.activeTodos().length); // 1
store.undo(); // toggleTodo undone
console.log(store.completedCount()); // 0
await store.fetchTodos();