Selectors for Redux - memoized functions for computing derived data from state.
—
Multiple memoization implementations optimized for different use cases and performance characteristics.
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])
}
);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;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 upExperimental 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:
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 }
);lruMemoize:
weakMapMemoize:
unstable_autotrackMemoize:
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
})
);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