Docs/State & reactivityv1.0.0-rc.1

Selectors — API Reference

@tagna/udiot v1.0.0-rc.1


import {
  createSelector,
  createParameterizedSelector,
  createSelectorObservable,
  batchSelect,
  SelectorFactory,
  deepEqual,
  referenceEqual
} from '@tagna/udiot';

createSelector

// Single function form — selector IS the combiner
function createSelector<TState, TResult>(
  fn: (state: TState) => TResult,
  options?: SelectorOptions
): MemoizedSelector<TState, TResult>

// Array form — selectors feed computed inputs to combiner
function createSelector<TState, T1, TResult>(
  selectors: [SelectorFn<TState, T1>],
  combiner: (arg1: T1) => TResult,
  options?: SelectorOptions
): MemoizedSelector<TState, TResult>

function createSelector<TState, T1, T2, TResult>(
  selectors: [SelectorFn<TState, T1>, SelectorFn<TState, T2>],
  combiner: (arg1: T1, arg2: T2) => TResult,
  options?: SelectorOptions
): MemoizedSelector<TState, TResult>

// (overloads continue up to 4 input selectors)

SelectorOptions

interface SelectorOptions {
  equalityFn?: (a: any, b: any) => boolean; // Default: shallow equality
  aiTracking?: boolean;                      // Track execution for AI analysis
  name?: string;                             // Name shown in warnings and DevTools
}

Memoization Behavior

On each call the selector computes the input values (by running the input selector array against the provided state). If every input is equal to the corresponding cached input (using equalityFn), the cached result is returned without calling the combiner. Otherwise the combiner runs, the cache is updated, and the new result is returned.

In the single-function form there are no input selectors — the combiner is called with the full state. Memoization caches the last result and only re-runs when the state reference changes.

The selector also logs a console warning (outside production) if it takes longer than 16 ms.


MemoizedSelector<TState, TResult>

The return type of createSelector. It is callable as a plain function and also exposes two utility methods.

interface MemoizedSelector<TState, TResult> {
  (state: TState): TResult;
  resetMemoization(): void;
  getMemoizationStats(): {
    hits: number;
    misses: number;
    lastResult: TResult | undefined;
  };
}

resetMemoization()

Clears the cached inputs and result. The next call will always run the combiner.

selectExpensiveView.resetMemoization();

getMemoizationStats()

Returns hit/miss counts and the last cached result. Useful for performance profiling.

const stats = selectExpensiveView.getMemoizationStats();
console.log(`hit rate: ${(stats.hits / (stats.hits + stats.misses) * 100).toFixed(1)}%`);

Equality Functions

referenceEqual (strict ===)

function referenceEqual(a: any, b: any): boolean

Returns true only if a and b are the same reference. This is the fastest check but will miss equality for new array/object literals with the same content.

deepEqual (recursive)

function deepEqual(a: any, b: any): boolean

Recursively compares objects and arrays by value. Slower than reference equality but avoids false cache misses when state is reconstructed from JSON or cloned.

The default equality function used by createSelector is a shallow equality check: objects are equal if they have the same keys and the same primitive (or identical reference) values at one level deep.

// Use deep equality for a selector over frequently-recreated objects
const selectConfig = createSelector(
  (state: AppState) => state.config,
  { equalityFn: deepEqual, name: 'selectConfig' }
);

SelectorFactory

A registry and builder for named selectors. Useful when you want to manage a collection of selectors as a unit and reset or inspect all of them at once.

class SelectorFactory<TState> {
  register<TResult>(name: string, selector: SelectorFn<TState, TResult>, options?: SelectorOptions): MemoizedSelector<TState, TResult>
  register<T1, TResult>(name: string, selectors: [...], combiner: (...) => TResult, options?: SelectorOptions): MemoizedSelector<TState, TResult>

  get<TResult>(name: string): MemoizedSelector<TState, TResult> | undefined
  has(name: string): boolean
  resetAll(): void
  getAllStats(): Map<string, ReturnType<MemoizedSelector<TState, any>['getMemoizationStats']>>
  clear(): void
}
const factory = new SelectorFactory<AppState>();

factory.register('selectTodos', (s: AppState) => s.todos);
factory.register(
  'selectCompletedCount',
  [factory.get<Todo[]>('selectTodos')!],
  (todos: Todo[]) => todos.filter(t => t.completed).length
);

// Reset all cached results (e.g., after a test)
factory.resetAll();

createParameterizedSelector

Creates a family of memoized selectors parameterized by runtime arguments. Each unique argument combination gets its own cached instance.

function createParameterizedSelector<TParams extends any[], TState, TResult>(
  selectorFactory: (...params: TParams) => SelectorFn<TState, TResult>,
  options?: SelectorOptions
): (...params: TParams) => MemoizedSelector<TState, TResult>

The parameter cache key is JSON.stringify(params), so parameters must be JSON-serializable.

const selectTodoById = createParameterizedSelector(
  (id: number) => (state: AppState) => state.todos.find(t => t.id === id)
);

// Each id gets its own memoized selector instance
const todo5 = selectTodoById(5)(state);
const todo9 = selectTodoById(9)(state);

batchSelect

Runs multiple selectors against the same state in a single call. Returns a typed tuple of results.

function batchSelect<TState, TResults extends any[]>(
  state: TState,
  ...selectors: SelectorFn<TState, any>[]
): TResults
const [todos, filter, loading] = batchSelect(
  store.getState(),
  s => s.todos,
  s => s.filter,
  s => s.loading
);

createSelectorObservable

Wraps a MemoizedSelector in an Observable that emits only when the selector's result changes. Uses reference equality (!==) to decide whether to emit.

function createSelectorObservable<TState, TResult>(
  state$: Observable<TState>,
  selector: MemoizedSelector<TState, TResult>
): Observable<TResult>
const selectCompletedTodos = createSelector(
  (s: TodoState) => s.todos,
  todos => todos.filter(t => t.completed)
);

const completed$ = createSelectorObservable(
  store.asObservable(),
  selectCompletedTodos
);

completed$.subscribe(list => renderList(list));

Composition Example

Three selectors forming a graph: raw state slices → derived collection → display-ready string.

interface AppState {
  todos: { id: number; text: string; completed: boolean }[];
  filter: 'all' | 'active' | 'completed';
}

// Layer 1 — raw state slices
const selectTodos  = (state: AppState) => state.todos;
const selectFilter = (state: AppState) => state.filter;

// Layer 2 — filtered collection
const selectVisibleTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'active':    return todos.filter(t => !t.completed);
      case 'completed': return todos.filter(t => t.completed);
      default:          return todos;
    }
  },
  { name: 'selectVisibleTodos' }
);

// Layer 3 — display-ready string (depends on layer 2)
const selectStatusLine = createSelector(
  [selectVisibleTodos, selectFilter],
  (visible, filter) =>
    `Showing ${visible.length} ${filter === 'all' ? '' : filter} item(s)`,
  { name: 'selectStatusLine' }
);

// Usage
const state: AppState = {
  todos: [
    { id: 1, text: 'Buy milk', completed: true },
    { id: 2, text: 'Walk dog', completed: false }
  ],
  filter: 'active'
};

console.log(selectVisibleTodos(state)); // [{ id: 2, ... }]
console.log(selectStatusLine(state));   // "Showing 1 active item(s)"

// Calling again with the same state object — both cache-hit, no recomputation
console.log(selectStatusLine(state));

When todos or filter changes, selectVisibleTodos recomputes first. If its output changes (by reference), selectStatusLine recomputes next. If neither input to selectStatusLine changes, the string is returned from cache.

From the canonical UDIOT public documentation.Try it in the playground ↗