reselect
Reselect builds memoized selector functions. You give createSelector a list of input selectors that pull raw slices out of your state, plus a result function that turns those slices into derived data. The generated selector only re-runs the result function when one of the inputs changes by reference, and otherwise hands back the exact same object it returned last time. That reference stability is the real point: React-Redux and React skip re-rendering when a value is identical, so a selector that filters or sorts an array stops causing renders on every unrelated dispatch. It is most associated with Redux but takes plain functions and works with any immutable state. It also ships inside Redux Toolkit, so many people use it without ever installing it.
Still the default answer for derived state in Redux apps, and v5's weakMapMemoize removed the cache-size-1 trap that made parameterized selectors painful. If you use Redux Toolkit you already have it, and if you do not use Redux you probably do not need it.
Use it if
- You store normalized state and components need derived views of it: filtered todo lists, sorted tables, totals, or joins across two slices that would otherwise recompute on every dispatch
- A component re-renders constantly because useSelector returns a fresh array or object each time; a memoized selector returns the same reference until the underlying data actually changes
- The derivation is genuinely expensive (large array transforms, grouping thousands of rows) and you want it computed once per state change rather than once per subscribed component
- You want composable derivation: selectors take other selectors as inputs, so selectVisibleTodoCount can build on selectVisibleTodos without duplicating the filter logic
- You call the same selector with different arguments across list items, like selectItemById(state, id); the v5 default weakMapMemoize caches per argument instead of only remembering the last call
- You are already on Redux Toolkit. createSelector is re-exported from @reduxjs/toolkit, and adding reselect to package.json invites two copies at different versions in one tree for no benefit
- You are not using Redux. Zustand, Jotai, Valtio, and TanStack Query each have their own derivation and equality story, and bolting selectors on top usually adds a layer rather than removing renders. On React 19 with the compiler, a lot of what selectors were doing manually is handled for you
- The result function is cheap or is basically an identity. Wrapping state => state.user.name in createSelector adds cache bookkeeping and argument comparison to something that was already free; the library ships an identityFunctionCheck dev warning precisely because people do this constantly
- Your input selectors do not return stable references. If an input selector returns state.items.map(...) or a fresh object literal, memoization never hits and the selector is pure overhead. This is the single most common reason a selector 'does not work', and the inputStabilityCheck dev warning exists to catch it
- You pass object arguments like selectThing(state, { id }). weakMapMemoize keys its cache on argument identity, so a new object literal per render is a permanent cache miss while still holding entries; you must pass primitives or reach for a keyed selector library
- You need deep nesting in TypeScript. v5 pushed the limit from roughly 8 to roughly 30 nested output selectors, but past that you still hit 'Type instantiation is excessively deep and possibly infinite' and end up annotating types by hand
Setup reality
npm install reselect is about as painless as it gets: zero runtime dependencies, roughly 7KB minified, dual CJS and ESM, TypeScript types included and TypeScript 4.7 or later required. The real setup cost is behavioral. In development, createSelector runs two checks the first time each selector is called (inputStabilityCheck calls your input selectors twice and warns if results differ, identityFunctionCheck warns if the result function just returns its input), so a working app can suddenly print console warnings after upgrading; you turn them off per selector with devModeChecks or globally with setGlobalDevModeChecks. v5 also changed the default memoizer from the old cache-size-1 LRU to weakMapMemoize, so tuning advice from older articles about maxSize and createSelectorCreator no longer applies, and defaultMemoize was renamed to lruMemoize. If you are on Redux Toolkit you already have it and should import from @reduxjs/toolkit rather than installing separately.
Patterns
Create a selector that skips recomputationbasic-memoized-selector
import { createSelector } from 'reselect'
const selectTodos = (state: RootState) => state.todos
export const selectCompletedTodos = createSelector(
[selectTodos],
todos => todos.filter(t => t.completed)
)
selectCompletedTodos(state) === selectCompletedTodos(state) // trueThe array form for input selectors is the documented style and gives better TypeScript inference than passing them variadically. Identical references out means React-Redux will not re-render subscribers.
Build selectors out of other selectorscompose-selectors
const selectFilter = (state: RootState) => state.filters.status
export const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, status) =>
status === 'all' ? todos : todos.filter(t => t.status === status)
)
export const selectVisibleCount = createSelector(
[selectVisibleTodos],
todos => todos.length
)Composition is the point: selectVisibleCount only recomputes when selectVisibleTodos returns a new array. Do not inline the filter logic in both places, or the two will drift and both will recompute independently.
Pass an id or other argument through to the selectorparameterized-selector
export const selectTodoById = createSelector(
[selectTodos, (state: RootState, id: string) => id],
(todos, id) => todos.find(t => t.id === id)
)
selectTodoById(state, 'abc')Every input selector receives all the arguments, so the second one is just an extractor. Pass primitives: an object argument like { id } creates a new reference each call and weakMapMemoize will miss the cache every time.
Stop annotating RootState on every input selectortyped-selector-creator
import { createSelector } from 'reselect'
export const createAppSelector = createSelector.withTypes<RootState>()
export const selectPostById = createAppSelector(
[state => state.posts.entities, (state, id: number) => id],
(entities, id) => entities[id]
)Added in 5.1. Parameter inference only works when the input selectors are wrapped in an array; with the variadic form you have to annotate extra parameters manually because of a TypeScript limitation.
Select several values as one objectstructured-selector
import { createStructuredSelector } from 'reselect'
export const selectHeaderProps = createStructuredSelector({
userName: (state: RootState) => state.user.name,
unread: (state: RootState) => state.alerts.unreadCount,
})
const { userName, unread } = selectHeaderProps(state)The returned object is memoized as a whole, so it stays reference-stable until one of the fields changes. Doing the same thing by hand with an object literal in useSelector produces a new object every render and re-renders on every action.
Keep the same reference when the value is equalstabilize-new-references
import { createSelector, weakMapMemoize } from 'reselect'
import { shallowEqual } from 'react-redux'
export const selectTodoIds = createSelector(
[selectTodos],
todos => todos.map(t => t.id),
{
memoize: weakMapMemoize,
memoizeOptions: { resultEqualityCheck: shallowEqual },
}
)A result function that maps or spreads returns a brand new array on every recomputation, so consumers re-render even when nothing meaningful changed. resultEqualityCheck compares the new result against the previous one and keeps the old reference when they match, at the cost of running the comparison.
Use the old LRU memoizer with a bounded cachelru-cache-size
import { createSelector, lruMemoize } from 'reselect'
export const selectExpensiveReport = createSelector(
[selectRows, (state: RootState, month: string) => month],
(rows, month) => buildReport(rows, month),
{
memoize: lruMemoize,
memoizeOptions: { maxSize: 12 },
}
)lruMemoize is the v4 defaultMemoize under a clearer name, and its default cache size is 1. Choose it over weakMapMemoize when you want a hard ceiling on retained results rather than a cache keyed by argument identity.
Apply the same memoization settings across a codebasecustom-selector-creator
import { createSelectorCreator, lruMemoize } from 'reselect'
import { isEqual } from 'lodash'
export const createDeepEqualSelector = createSelectorCreator({
memoize: lruMemoize,
memoizeOptions: { equalityCheck: isEqual, maxSize: 5 },
})
export const selectConfig = createDeepEqualSelector(
[(state: RootState) => state.config],
config => normalize(config)
)Deep equality on inputs is a trade: you pay isEqual on every call to avoid a recomputation. It is worth it only when the result function is much more expensive than the comparison, which is rarer than people assume.
Turn off a development-mode warningsilence-dev-checks
import { createSelector, setGlobalDevModeChecks } from 'reselect'
// per selector
const selectThing = createSelector([selectRaw], derive, {
devModeChecks: { inputStabilityCheck: 'never' },
})
// or globally, once at startup
setGlobalDevModeChecks({ identityFunctionCheck: 'never' })The checks run only in development and by default only on the first call ('once'); 'always' is useful while debugging. Silence them only after you understand the warning, since inputStabilityCheck firing almost always means memoization is not working at all.
Find out whether a selector is actually memoizingdebug-recomputations
selectVisibleTodos(state)
selectVisibleTodos(state)
console.log(selectVisibleTodos.recomputations()) // 1
console.log(selectVisibleTodos.dependencyRecomputations()) // 1
selectVisibleTodos.resetRecomputations()
selectVisibleTodos.clearCache()recomputations counts result function runs; dependencyRecomputations counts input selector runs, which is how you tell whether the argument memoization or the result memoization is the one missing. Both live on the selector object and are safe to read in tests.
Test the derivation without building a whole stateunit-test-result-function
import { selectVisibleTodos } from './selectors'
test('filters by status', () => {
const todos = [{ id: '1', status: 'done' }, { id: '2', status: 'open' }]
expect(selectVisibleTodos.resultFunc(todos, 'done')).toHaveLength(1)
})resultFunc is the raw unmemoized function, so calling it takes the input selectors' outputs directly instead of a RootState. It also bypasses the cache, which is what you want in tests where memoization between cases would hide bugs.
Consume a selector from React-Reduxuse-in-react-component
import { useSelector } from 'react-redux'
function TodoList({ status }: { status: string }) {
const todos = useSelector((state: RootState) =>
selectTodosByStatus(state, status)
)
return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
}With the v5 default memoizer a single shared selector handles many components with different arguments, so the old per-component makeSelector factory pattern is no longer needed. The inline arrow passed to useSelector is fine because the memoized selector inside it is what provides reference stability.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @reduxjs/toolkit | npm | You are writing Redux at all; it bundles this exact library plus createSlice and the entity adapter selectors you were about to hand-write. |
| proxy-memoize | npm | You want memoization based on which properties were actually touched, so you can write a plain function instead of splitting input selectors and a result function. |
| re-reselect | npm | You need an explicit cache key per selector call and want control over eviction rather than relying on weakMapMemoize's reference-keyed cache. |