Skip to main content

Development-Only Stability Checks

Reselect includes extra checks in development mode to help catch and warn about mistakes in selector behavior.

inputStabilityCheck

Due to how "Cascading Memoization" works in Reselect, it is crucial that your input selectors do not return a new reference on each run. If an input selector always returns a new reference, like

state => ({ a: state.a, b: state.b })

or

state => state.todos.map(todo => todo.id)

that will cause the selector to never memoize properly. Since this is a common mistake, we've added a development mode check to catch this. By default, createSelector will now run the input selectors twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values.

type DevModeCheckFrequency = 'always' | 'once' | 'never'
Possible ValuesDescription
onceRun only the first time the selector is called.
alwaysRun every time the selector is called.
neverNever run the input stability check.
info

The input stability check is automatically disabled in production environments.

You can configure this behavior in two ways:

1. Globally through setGlobalDevModeChecks:

A setGlobalDevModeChecks function is exported from Reselect, which should be called with the desired setting.

import { setGlobalDevModeChecks } from 'reselect'

// Run only the first time the selector is called. (default)
setGlobalDevModeChecks({ inputStabilityCheck: 'once' })

// Run every time the selector is called.
setGlobalDevModeChecks({ inputStabilityCheck: 'always' })

// Never run the input stability check.
setGlobalDevModeChecks({ inputStabilityCheck: 'never' })

2. Per selector by passing an inputStabilityCheck option directly to createSelector:

development-only-stability-checks/inputStabilityCheck.ts
import { createSelector } from 'reselect'

interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

// Create a selector that double-checks the results of input selectors every time it runs.
const selectCompletedTodosLength = createSelector(
[
// ❌ Incorrect Use Case: This input selector will not be
// memoized properly since it always returns a new reference.
(state: RootState) =>
state.todos.filter(({ completed }) => completed === true),
],
completedTodos => completedTodos.length,
// Will override the global setting.
{ devModeChecks: { inputStabilityCheck: 'always' } },
)
warning

This will override the global input stability check set by calling setGlobalDevModeChecks.

identityFunctionCheck

When working with Reselect, it's crucial to adhere to a fundamental philosophy regarding the separation of concerns between extraction and transformation logic.

  • Extraction Logic: This refers to operations like state => state.todos, which should be placed in [input selectors]. Extraction logic is responsible for retrieving or 'selecting' data from a broader state or dataset.

  • Transformation Logic: In contrast, transformation logic, such as todos => todos.map(({ id }) => id), belongs in the [result function]. This is where you manipulate, format, or transform the data extracted by the input selectors.

Most importantly, effective memoization in Reselect hinges on following these guidelines. Memoization, only functions correctly when extraction and transformation logic are properly segregated. By keeping extraction logic in input selectors and transformation logic in the result function, Reselect can efficiently determine when to reuse cached results and when to recompute them. This not only enhances performance but also ensures the consistency and predictability of your selectors.

For memoization to work as intended, it's imperative to follow both guidelines. If either is disregarded, memoization will not function properly. Consider the following example for clarity:

// ❌ Incorrect Use Case: This will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
// ✔️ GOOD: Contains extraction logic.
[(state: RootState) => state.todos],
// ❌ BAD: Does not contain transformation logic.
todos => todos,
)
type DevModeCheckFrequency = 'always' | 'once' | 'never'
Possible ValuesDescription
onceRun only the first time the selector is called.
alwaysRun every time the selector is called.
neverNever run the identity function check.
info

The identity function check is automatically disabled in production environments.

You can configure this behavior in two ways:

1. Globally through setGlobalDevModeChecks:

import { setGlobalDevModeChecks } from 'reselect'

// Run only the first time the selector is called. (default)
setGlobalDevModeChecks({ identityFunctionCheck: 'once' })

// Run every time the selector is called.
setGlobalDevModeChecks({ identityFunctionCheck: 'always' })

// Never run the identity function check.
setGlobalDevModeChecks({ identityFunctionCheck: 'never' })

2. Per selector by passing an identityFunctionCheck option directly to createSelector:

development-only-stability-checks/identityFunctionCheck.ts
import { createSelector } from 'reselect'

interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

// Create a selector that checks to see if the result function is an identity function.
const selectTodos = createSelector(
// ✔️ GOOD: Contains extraction logic.
[(state: RootState) => state.todos],
// ❌ BAD: Does not contain transformation logic.
todos => todos,
// Will override the global setting.
{ devModeChecks: { identityFunctionCheck: 'always' } },
)
warning

This will override the global identity function check set by calling setGlobalDevModeChecks.