CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-reselect

Selectors for Redux - memoized functions for computing derived data from state.

Pending
Overview
Eval results
Files

memoization.mddocs/

Memoization Strategies

Multiple memoization implementations optimized for different use cases and performance characteristics.

Capabilities

lruMemoize

LRU (Least Recently Used) cache-based memoization with configurable cache size and equality checking.

/**
 * Creates a memoized function using LRU caching strategy
 * @param func - Function to memoize
 * @param options - Configuration options for LRU cache
 * @returns Memoized function with clearCache method
 */
function lruMemoize<Args extends readonly unknown[], Return>(
  func: (...args: Args) => Return,
  options?: LruMemoizeOptions
): ((...args: Args) => Return) & DefaultMemoizeFields;

interface LruMemoizeOptions<Result = any> {
  /** Maximum number of results to cache (default: 1) */
  maxSize?: number;
  
  /** Function to check equality of cache keys (default: referenceEqualityCheck) */
  equalityCheck?: EqualityFn;
  
  /** Function to compare newly generated output value against cached values */
  resultEqualityCheck?: EqualityFn<Result>;
}

Basic Usage:

import { lruMemoize } from "reselect";

// Simple memoization with default options
const memoizedExpensiveFunction = lruMemoize((data) => {
  return performExpensiveComputation(data);
});

// With custom cache size
const memoizedWithLargerCache = lruMemoize(
  (data) => processData(data),
  { maxSize: 10 }
);

// Usage
const result = memoizedExpensiveFunction(someData);
console.log(memoizedExpensiveFunction.resultsCount()); // 1

// Call again with same data (should not recompute)
const result2 = memoizedExpensiveFunction(someData);
console.log(memoizedExpensiveFunction.resultsCount()); // Still 1

// Call with different data
const result3 = memoizedExpensiveFunction(otherData);
console.log(memoizedExpensiveFunction.resultsCount()); // 2

// Reset results count
memoizedExpensiveFunction.resetResultsCount();
console.log(memoizedExpensiveFunction.resultsCount()); // 0

// Clear the cache
memoizedExpensiveFunction.clearCache();

With Custom Equality Check:

import { lruMemoize } from "reselect";

// Custom equality check for objects
const deepEqualMemoized = lruMemoize(
  (obj) => transformObject(obj),
  {
    maxSize: 5,
    equalityCheck: (a, b) => JSON.stringify(a) === JSON.stringify(b)
  }
);

// Custom equality for specific object properties
const userMemoized = lruMemoize(
  (user) => processUser(user),
  {
    equalityCheck: (a, b) => a.id === b.id && a.version === b.version
  }
);

// With result equality check to handle cases where input changes but output is the same
const todoIdsMemoized = lruMemoize(
  (todos) => todos.map(todo => todo.id),
  {
    maxSize: 3,
    resultEqualityCheck: (a, b) => 
      a.length === b.length && a.every((id, index) => id === b[index])
  }
);

referenceEqualityCheck

Default equality function used by lruMemoize for comparing cache keys.

/**
 * Reference equality check function (===)
 * @param a - First value to compare
 * @param b - Second value to compare  
 * @returns True if values are reference equal
 */
function referenceEqualityCheck(a: any, b: any): boolean;

weakMapMemoize

WeakMap-based memoization that automatically garbage collects unused cache entries when objects are no longer referenced.

/**
 * Creates a memoized function using WeakMap caching strategy
 * @param func - Function to memoize
 * @param options - Configuration options for WeakMap cache
 * @returns Memoized function with clearCache method
 */
function weakMapMemoize<Args extends readonly unknown[], Return>(
  func: (...args: Args) => Return,
  options?: WeakMapMemoizeOptions
): ((...args: Args) => Return) & DefaultMemoizeFields;

interface WeakMapMemoizeOptions<Result = any> {
  /** Function to compare newly generated output value against cached values */
  resultEqualityCheck?: EqualityFn<Result>;
}

Basic Usage:

import { weakMapMemoize } from "reselect";

// WeakMap memoization (default for createSelector)
const memoizedProcessor = weakMapMemoize((objects) => {
  return objects.map(obj => processObject(obj));
});

// Check results count
const result = memoizedProcessor(someObjects);
console.log(memoizedProcessor.resultsCount()); // 1

// Clear cache and reset count
memoizedProcessor.clearCache(); // Also resets results count
console.log(memoizedProcessor.resultsCount()); // 0

// With result equality check
const customWeakMapMemoized = weakMapMemoize(
  (data) => transformData(data),
  {
    resultEqualityCheck: (a, b) => a.length === b.length && a.every((item, i) => item.id === b[i].id)
  }
);

Automatic Garbage Collection:

import { weakMapMemoize } from "reselect";

const processObjects = weakMapMemoize((objectArray) => {
  return objectArray.map(obj => expensiveTransform(obj));
});

// Objects are automatically garbage collected when no longer referenced
let objects = [{ id: 1 }, { id: 2 }];
const result1 = processObjects(objects); // Cached

objects = null; // Original objects can be garbage collected
// Cache entries for those objects are automatically cleaned up

unstable_autotrackMemoize

Experimental auto-tracking memoization using Proxy to track nested field access patterns.

/**
 * Experimental memoization that tracks which nested fields are accessed
 * @param func - Function to memoize
 * @returns Memoized function with clearCache method
 */
function unstable_autotrackMemoize<Func extends AnyFunction>(
  func: Func
): Func & DefaultMemoizeFields;

Basic Usage:

import { unstable_autotrackMemoize } from "reselect";

// Auto-tracking memoization
const autotrackProcessor = unstable_autotrackMemoize((state) => {
  // Only recomputes if state.users[0].profile.name changes
  return state.users[0].profile.name.toUpperCase(); 
});

// With createSelector
import { createSelector } from "reselect";

const selectUserName = createSelector(
  [(state) => state.users],
  (users) => users[0]?.profile?.name, // Tracks specific field access
  { memoize: unstable_autotrackMemoize }
);

Design Tradeoffs:

  • Pros: More precise memoization, avoids excess calculations, fewer re-renders
  • Cons: Cache size of 1, slower than lruMemoize, unexpected behavior with non-accessing selectors
  • Use Case: Nested field access where you want to avoid recomputation when unrelated fields change

Important Limitations:

// This selector will NEVER update because it doesn't access any fields
const badSelector = createSelector(
  [(state) => state.todos],
  (todos) => todos, // Just returns the value directly - no field access
  { memoize: unstable_autotrackMemoize }
);

// This works correctly because it accesses fields
const goodSelector = createSelector(
  [(state) => state.todos],
  (todos) => todos.map(todo => todo.id), // Accesses .map and .id
  { memoize: unstable_autotrackMemoize }
);

Performance Comparison

When to Use Each Strategy

lruMemoize:

  • Multiple argument combinations need caching
  • Predictable cache eviction behavior needed
  • Working with primitive values or need custom equality logic
  • Default choice for most use cases

weakMapMemoize:

  • Working primarily with object references
  • Want automatic garbage collection
  • Large numbers of different object combinations
  • Memory efficiency is important

unstable_autotrackMemoize:

  • Accessing specific nested fields in large objects
  • Want to avoid recomputation when unrelated fields change
  • Can accept cache size limitation of 1
  • Performance testing shows it's beneficial for your use case

Usage Examples in Selectors

import { 
  createSelector, 
  createSelectorCreator,
  lruMemoize, 
  weakMapMemoize, 
  unstable_autotrackMemoize 
} from "reselect";

// LRU memoization for selectors with multiple cache entries
const createLRUSelector = createSelectorCreator({
  memoize: lruMemoize,
  memoizeOptions: { maxSize: 50 }
});

const selectFilteredItems = createLRUSelector(
  [selectItems, selectFilters],
  (items, filters) => applyFilters(items, filters)
);

// WeakMap memoization (default)
const selectProcessedUsers = createSelector(
  [selectUsers],
  (users) => users.map(user => processUser(user))
);

// Auto-tracking for nested field access
const createAutotrackSelector = createSelectorCreator({
  memoize: unstable_autotrackMemoize
});

const selectSpecificUserData = createAutotrackSelector(
  [(state) => state],
  (state) => ({
    name: state.users.currentUser.profile.displayName,
    avatar: state.users.currentUser.profile.avatar.url
  })
);

Types

interface DefaultMemoizeFields {
  /** Clears the memoization cache */
  clearCache: () => void;
  /** Returns the number of times the memoized function has computed results */
  resultsCount: () => number;
  /** Resets the results count to 0 */
  resetResultsCount: () => void;
}

type EqualityFn<T = any> = (a: T, b: T) => boolean;

type AnyFunction = (...args: any[]) => any;

interface Cache {
  get(key: unknown): unknown | typeof NOT_FOUND;
  put(key: unknown, value: unknown): void;
  getEntries(): Array<{ key: unknown; value: unknown }>;
  clear(): void;
}

Install with Tessl CLI

npx tessl i tessl/npm-reselect

docs

development.md

index.md

memoization.md

selector-creation.md

selector-creator.md

structured-selectors.md

tile.json