Skip to main content

weakMapMemoize

lruMemoize has to be explicitly configured to have a cache size larger than 1, and uses an LRU cache internally.

weakMapMemoize creates a tree of WeakMap-based cache nodes based on the identity of the arguments it's been called with (in this case, the extracted values from your input selectors). This allows weakMapMemoize to have an effectively infinite cache size. Cache results will be kept in memory as long as references to the arguments still exist, and then cleared out as the arguments are garbage-collected.

Default since v5.0.0

Since v5.0.0, weakMapMemoize is the default memoization function used by createSelector for both memoize and argsMemoize. In v4.x the default was lruMemoize (previously named defaultMemoize) with a cache size of 1.

This means you generally do not need to pass memoize: weakMapMemoize or argsMemoize: weakMapMemoize yourself. The examples on this page pass them explicitly only to illustrate the behavior — a plain createSelector(...) call already uses weakMapMemoize.

API Reference

Parameters

NameDescription
funcThe function to be memoized.
options.resultEqualityCheckOptional. Compares a newly computed result against the previous one. If they compare equal, the previous result is returned instead, preserving reference equality for downstream consumers.
options.maxSizeOptional. Bounds how many results keyed by primitive arguments are retained. Available since 5.3.0. See Bounding cache size with maxSize below for the semantics.

Returns

A memoized function with a .clearCache() method attached.

Type Parameters

NameDescription
FuncThe type of the function that is memoized.

Usage Guide

Design Tradeoffs

  • Pros:

    • It has an effectively infinite cache size, but you have no control over how long values are kept in cache as it's based on garbage collection and WeakMaps.
  • Cons:

    • There's currently no way to alter the argument comparisons. They're based on strict reference equality.
    • Garbage collection only frees results keyed by object or function arguments. Results keyed by primitive arguments (strings, numbers, booleans) are held strongly, and are only released by calling .clearCache(). A selector that keeps seeing new primitive values in the same argument position — ids, offsets, timestamps — grows its cache without bound. In development mode, the cacheSizeCheck warns when a single argument position passes 1,000 distinct primitive values. As of 5.3.0, the maxSize option bounds this growth.

Use Cases

  • This memoizer is likely best used for cases where you need to call the same selector instance with many different arguments, such as a single selector instance that is used in a list item component and called with item IDs like:
useSelector(state => selectSomeData(state, id))

Prior to weakMapMemoize, you had this problem:

weakMapMemoize/cacheSizeProblem.ts
import { createSelector } from 'reselect'

export interface RootState {
items: { id: number; category: string; name: string }[]
}

const state: RootState = {
items: [
{ id: 1, category: 'Electronics', name: 'Wireless Headphones' },
{ id: 2, category: 'Books', name: 'The Great Gatsby' },
{ id: 3, category: 'Home Appliances', name: 'Blender' },
{ id: 4, category: 'Stationery', name: 'Sticky Notes' }
]
}

const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)

selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics') // Selector runs again!

Before you could solve this in a number of different ways:

  1. Set the maxSize with lruMemoize:
weakMapMemoize/setMaxSize.ts
import { createSelector, lruMemoize } from 'reselect'
import type { RootState } from './cacheSizeProblem'

const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoize: lruMemoize,
memoizeOptions: {
maxSize: 10
}
}
)

But this required having to know the cache size ahead of time.

  1. Create unique selector instances using useMemo.
weakMapMemoize/withUseMemo.tsx
import type { FC } from 'react'
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
import type { RootState } from './cacheSizeProblem'

const makeSelectItemsByCategory = (category: string) =>
createSelector([(state: RootState) => state.items], items =>
items.filter(item => item.category === category)
)

interface Props {
category: string
}

const MyComponent: FC<Props> = ({ category }) => {
const selectItemsByCategory = useMemo(
() => makeSelectItemsByCategory(category),
[category]
)

const itemsByCategory = useSelector(selectItemsByCategory)

return (
<div>
{itemsByCategory.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
  1. Use Re-reselect:
import { createCachedSelector } from 're-reselect'

const selectItemsByCategory = createCachedSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)((state: RootState, category: string) => category)

Starting in 5.0.0, you can eliminate this problem using weakMapMemoize.

weakMapMemoize/cacheSizeSolution.ts
import { createSelector, weakMapMemoize } from 'reselect'
import type { RootState } from './cacheSizeProblem'

const state: RootState = {
items: [
{ id: 1, category: 'Electronics', name: 'Wireless Headphones' },
{ id: 2, category: 'Books', name: 'The Great Gatsby' },
{ id: 3, category: 'Home Appliances', name: 'Blender' },
{ id: 4, category: 'Stationery', name: 'Sticky Notes' }
]
}

const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
}
)

selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics') // Cached
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics') // Still cached!

This solves the problem of having to know and set the cache size prior to creating a memoized selector. Because weakMapMemoize essentially provides a dynamic cache size out of the box.

Bounding cache size with maxSize

The infinite cache size eliminates earlier common pain points around reusing selector instances with varying arguments, which often required cumbersome workarounds like creating a unique selector instance per component instance. Each separate argument combination gets cached, and the code is simpler.

The tradeoff is that an "infinite cache size" can easily become a memory leak depending on arguments and usage patterns. Results keyed by object arguments are freed by garbage collection, but results keyed by primitive arguments are held strongly until you call .clearCache() (see Design Tradeoffs above). A selector that keeps seeing new primitive values in the same argument position — ids, offsets, timestamps, page numbers — grows its cache without bound.

As of 5.3.0, weakMapMemoize accepts a maxSize option that bounds this growth:

import { weakMapMemoize } from 'reselect'

const getVisibleItems = weakMapMemoize(
(items: Item[], from: number, to: number) => items.slice(from, to),
{ maxSize: 100 }
)

This is implemented using a "generational" approach, rather than the LRU approach used by lruMemoize:

  • Only results keyed by primitive arguments count toward maxSize. Results keyed by objects or functions are already released by garbage collection.
  • After maxSize new results have been cached, the whole cache is demoted to a "previous generation", and later results build up a fresh cache. When the fresh cache fills up in turn, the previous generation is dropped in one step.
  • A call that finds its result in the previous generation promotes that result into the current one, so entries that keep getting used keep surviving the flips.
  • Total retention is therefore bounded at roughly 2 * maxSize results, and eviction happens in batches rather than one entry at a time. This is by design: it keeps the per-call bookkeeping near zero, and there is no cost at all when maxSize is not passed.

If you need an exact bound with per-entry LRU eviction order, use lruMemoize instead — the tradeoff is that its maxSize is exact but its bookkeeping runs on every call.

Bounding a createSelector selector requires both options

createSelector memoizes at two levels: argsMemoize caches by the selector's call arguments, and memoize caches by the input selector results. If you only set memoizeOptions: {maxSize}, the args cache still grows without bound — and because it sits in front, it returns cached results before the bounded cache is ever consulted. Pass maxSize in both options:

const selectVisibleItems = createSelector(
[
(state: RootState) => state.items,
(state: RootState, from: number) => from,
(state: RootState, from: number, to: number) => to
],
(items, from, to) => items.slice(from, to),
{
memoizeOptions: { maxSize: 100 },
argsMemoizeOptions: { maxSize: 100 }
}
)

Since weakMapMemoize is the default for both levels, no memoize or argsMemoize fields are needed — the options objects configure the defaults.

In development mode, the cacheSizeCheck warns when a selector shows the unbounded-growth pattern, which is a good signal that a particular selector would benefit from maxSize.

Examples

Using weakMapMemoize with createSelector

weakMapMemoize/usingWithCreateSelector.ts
import { createSelector, weakMapMemoize } from 'reselect'
import type { RootState } from './cacheSizeProblem'

const state: RootState = {
items: [
{ id: 1, category: 'Electronics', name: 'Wireless Headphones' },
{ id: 2, category: 'Books', name: 'The Great Gatsby' },
{ id: 3, category: 'Home Appliances', name: 'Blender' },
{ id: 4, category: 'Stationery', name: 'Sticky Notes' }
]
}

const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
}
)

selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics')

Using weakMapMemoize with createSelectorCreator

weakMapMemoize/usingWithCreateSelectorCreator.ts
import { createSelectorCreator, weakMapMemoize } from 'reselect'
import type { RootState } from './cacheSizeProblem'

const state: RootState = {
items: [
{ id: 1, category: 'Electronics', name: 'Wireless Headphones' },
{ id: 2, category: 'Books', name: 'The Great Gatsby' },
{ id: 3, category: 'Home Appliances', name: 'Blender' },
{ id: 4, category: 'Stationery', name: 'Sticky Notes' }
]
}

const createSelectorWeakMap = createSelectorCreator({
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
})

const selectItemsByCategory = createSelectorWeakMap(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)

selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics')