Deriving Data with Selectors
- Why good Redux architecture keeps state minimal and derives additional data
- Principles of using selector functions to derive data and encapsulate lookups
- How to use the Reselect library to write memoized selectors for optimization
- How memoized selectors work with
useSelectorand component arguments - Additional tools and libraries for creating selectors
- Best practices for writing selectors
Deriving Data
We specifically recommend that Redux apps should keep the Redux state minimal, and derive additional values from that state whenever possible.
This includes things like calculating filtered lists or summing up values. As an example, a todo app would keep an original list of todo objects in state, but derive a filtered list of todos outside the state whenever the state is updated. Similarly, a check for whether all todos have been completed, or number of todos remaining, can be calculated outside the store as well.
This has several benefits:
- The actual state is easier to read
- Less logic is needed to calculate those additional values and keep them in sync with the rest of the data
- The original state is still there as a reference and isn't being replaced
This is also a good principle for React state as well! Many times users tried to define a useEffect hook that waits for a state value to change, and then sets state with some derived value like setAllCompleted(allCompleted). Instead, that value can be derived during the rendering process and used directly, without having to save the value into state at all:
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
Calculating Derived Data with Selectors
In a typical Redux application, the logic for deriving data is usually written as functions we call selectors.
Selectors are primarily used to encapsulate logic for looking up specific values from state, logic for actually deriving values, and improving performance by avoiding unnecessary recalculations.
You are not required to use selectors for all state lookups, but they are a standard pattern and widely used.
Basic Selector Concepts
A "selector function" is any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state.
Selectors don't have to be written using a special library, and it doesn't matter whether you write them as arrow functions or the function keyword. For example, all of these are valid selector functions:
// Arrow function, direct lookup
const selectEntities = (state: RootState) => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state: RootState) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state: RootState) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items: Item[], namePrefix: string) =>
items.filter(item => item.name.startsWith(namePrefix))
A selector function can have any name you want. However, we recommend prefixing selector function names with the word select combined with a description of the value being selected. Typical examples of this would look like selectTodoById, selectFilteredTodos, and selectVisibleTodos.
If you've used the useSelector hook from React-Redux, you're probably already familiar with the basic idea of a selector function - the functions that we pass to useSelector must be selectors:
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useAppSelector(state => state.todos)
}
Selector functions are typically defined in two different parts of a Redux application:
- In slice files, alongside the reducer logic
- In component files, either outside the component, or inline in
useSelectorcalls
A selector function can be used anywhere you have access to the entire Redux root state value. This includes the useSelector hook, middleware, thunks, listeners, and sagas. For example, thunks and middleware have access to the getState argument, so you can call a selector there:
function addTodosIfAllowed(todoText: string): AppThunk {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
It's not typically possible to use selectors inside of reducers, because a slice reducer only has access to its own slice of the Redux state, and most selectors expect to be given the entire Redux root state as an argument.
Reading State Once
React components should normally use useSelector, because it subscribes to the store and updates the component when the selected value changes. Sometimes code outside React only has access to dispatch and needs a one-time read of the latest state without subscribing. In that case, you can dispatch a small thunk that calls the selector with getState() and returns its result:
const selectFromState = <Selected>(
selector: (state: RootState) => Selected
): AppThunk<Selected> => {
return (_dispatch, getState) => selector(getState())
}
const users = dispatch(selectFromState(selectUsers))
This reads the state at the moment the thunk runs. The returned value does not stay updated, so use a subscription API such as useSelector when the caller needs to react to later state changes.
Encapsulating State Shape with Selectors
The first reason to use selector functions is for encapsulation and reusability when dealing with your Redux state shape.
Let's say that one of your useSelector hooks makes a very specific lookup into part of your Redux state:
const data = useAppSelector(state => state.some.deeply.nested.field)
That is legal code, and will run fine. But, it might not be the best idea architecturally. Imagine that you've got several components that need to access that field. What happens if you need to make a change to where that piece of state lives? You would now have to go change every useSelector hook that references that value. So, in the same way that we recommend using action creators to encapsulate details of creating actions, we recommend defining reusable selectors to encapsulate the knowledge of where a given piece of state lives. Then, you can use a given selector function many times in the codebase, anywhere that your app needs to retrieve that particular data.
Ideally, only your reducer functions and selectors should know the exact state structure, so if you change where some state lives, you would only need to update those two pieces of logic.
Because of this, it's often a good idea to define reusable selectors directly inside slice files, rather than always defining them inside of a component.
One common description of selectors is that they're like "queries into your state". You don't care about exactly how the query came up with the data you needed, just that you asked for the data and got back a result.
Optimizing Selectors with Memoization
Selector functions often need to perform relatively "expensive" calculations, or create derived values that are new object and array references. This can be a concern for application performance, for several reasons:
- Selectors used with
useSelectorwill be re-run after every dispatched action, regardless of what section of the Redux root state was actually updated. Re-running expensive calculations when the input state sections didn't change is a waste of CPU time, and it's very likely that the inputs won't have changed most of the time anyway. useSelectorrelies on===reference equality checks of the return values to determine if the component needs to re-render. If a selector always returns new references, it will force the component to re-render even if the derived data is effectively the same as last time. This is especially common with array operations likemap()andfilter(), which return new array references.
As an example, this component is written badly, because its useSelector call always returns a new array reference. That means the component will re-render after every dispatched action, even if the input state.todos slice hasn't changed:
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useAppSelector(state =>
state.todos.filter(todo => todo.completed)
)
}
Another example is a component that needs to do some "expensive" work to transform data:
function ExampleComplexComponent() {
const data = useAppSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
Similarly, this "expensive" logic will re-run after every dispatched action. Not only will it probably create new references, but it's work that doesn't need to be done unless state.data actually changes.
Because of this, we need a way to write optimized selectors that can avoid recalculating results if the same inputs are passed in. This is where the idea of memoization comes in.
Memoization is a form of caching. It involves tracking inputs to a function, and storing the inputs and the results for later reference. If a function is called with the same inputs as before, the function can skip doing the actual work, and return the same result it generated the last time it received those input values. This optimizes performance by only doing work if inputs have changed, and consistently returning the same result references if the inputs are the same.
Next, we'll look at some options for writing memoized selectors.
Writing Memoized Selectors with Reselect
The Redux ecosystem uses a library called Reselect to create memoized selector functions. Reselect is a separate package, but createSelector and the other Reselect APIs are re-exported from Redux Toolkit, so you do not need to install it separately.
This page covers how memoized selectors fit into a Redux app. The Reselect docs are the reference for the library itself: how the memoization works internally, the createSelector API and its options, the memoization functions, and a FAQ.
createSelector Overview
Reselect's createSelector accepts one or more "input selector" functions, plus a "result function", and returns a new memoized selector.
When you call the generated selector, Reselect runs all of the input selectors with the arguments you passed, and compares their results to the results from the previous call. If any of the results are === different, it re-runs the result function with those values as its arguments. If all of the results are the same as last time, it skips the result function and returns the cached result from before.
In typical usage, the input selectors are simple functions that return values nested somewhere inside the state object, and the result function does the actual derivation work:
import { createSelector } from '@reduxjs/toolkit'
const selectTodos = (state: RootState) => state.todos.items
const selectCurrentUser = (state: RootState) => state.users.currentUser
const selectTodosForCurrentUser = createSelector(
[selectTodos, selectCurrentUser],
(todos, currentUser) => {
console.log('Result function running')
return todos.filter(todo => todo.ownerId === currentUser.userId)
}
)
const todosForCurrentUser1 = selectTodosForCurrentUser(state)
// Log: "Result function running"
const todosForCurrentUser2 = selectTodosForCurrentUser(state)
// No log output
console.log(todosForCurrentUser1 === todosForCurrentUser2)
// true
The second time we called selectTodosForCurrentUser, the result function didn't execute. The results of selectTodos and selectCurrentUser were the same as the first call, so selectTodosForCurrentUser returned the memoized result.
This means that input selectors should just extract and return values, and the result function should do the transformation work. A result function that just returns one of its inputs unchanged, or an input selector that is state => state, will not memoize anything useful. The Reselect docs cover these and other pitfalls in Best Practices and Common Mistakes, and Reselect's development-mode checks warn about both cases the first time a selector runs.
createSelector Behavior
Reselect memoizes in two layers. It first compares the arguments passed to the selector against the previous call, and if they are identical it returns the cached result without running anything. If the arguments differ (which they will after every dispatch, because the root state object is a new reference), it runs the input selectors and compares their results. Only if one of those changed does the result function run. The Reselect docs describe this in detail in "How Does Reselect Work?".
Reselect 5 memoizes with weakMapMemoize by default. It keeps a separate cache entry for each distinct set of arguments, keyed by reference, so calling a selector with several different inputs in a row does not evict earlier results:
const a = someSelector(state, 1) // first call: runs the result function
const b = someSelector(state, 1) // same inputs: cached
const c = someSelector(state, 2) // new inputs: runs the result function
const d = someSelector(state, 1) // still cached from the first call
Cache entries are held in WeakMaps keyed by the argument objects, so they are released when those objects are garbage collected. There is no size limit to configure.
Reselect 4 and earlier used lruMemoize with a cache size of 1, which only remembered the most recent set of arguments. In that version, c would have evicted the result for (state, 1), and d would have recalculated. lruMemoize is still available if you want a bounded cache, and the "Selector Factories" section below explains when it still matters.
Because every input selector receives the full argument list, all of the input selectors you provide should accept the same types of parameters:
const selectItems = (state: RootState) => state.items
// expects a number as the second argument
const selectItemId = (state: RootState, itemId: number) => itemId
// expects an object as the second argument
const selectOtherField = (
state: RootState,
someObject: { someField: string }
) => someObject.someField
// ❌ These input selectors disagree about what the second argument is
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
If you call selectItemById(state, 42), selectOtherField will break because it's trying to access 42.someField. TypeScript will report this mismatch; in plain JavaScript it fails at runtime.
Reselect Usage Patterns and Limitations
Nesting Selectors
It's possible to take selectors generated with createSelector, and use them as inputs for other selectors as well. In this example, the selectCompletedTodos selector is used as an input to selectCompletedTodoDescriptions:
const selectTodos = (state: RootState) => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
Passing Input Parameters
A Reselect-generated selector can be called with as many arguments as you want: selectThings(a, b, c, d, e). What matters for re-running the result function is not the arguments themselves, but whether the input selectors' results changed. So if you want to pass additional parameters through to the result function, you must define input selectors that extract those values from the original selector arguments:
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
(state: RootState) => state.items,
// Take the second arg, `category`, and forward to the result function
(state: RootState, category: string) => category
],
// Result function gets (items, category) as args
(items, category) => items.filter(item => item.category === category)
)
const electronicItems = selectItemsByCategory(state, 'electronics')
For consistency, you may want to consider passing additional parameters to a selector as a single object, such as selectThings(state, otherArgs), and then extracting values from the otherArgs object. See also the Reselect FAQ entry on selectors that take an argument.
Selector Factories
With Reselect 4's lruMemoize and its default cache size of 1, a single selector instance could only remember one set of arguments. If several components called selectItemsByCategory(state, category) with different categories, each call evicted the previous result and the result function re-ran every time. The workaround was a "selector factory" - a function that calls createSelector() and returns a fresh selector instance for each component:
const makeSelectItemsByCategory = () =>
createSelector(
[(state: RootState) => state.items, (state, category: string) => category],
(items, category) => items.filter(item => item.category === category)
)
With Reselect 5's default weakMapMemoize, one shared selector already keeps a cache entry per distinct argument set, so you usually do not need a factory. A factory is still useful if you have opted back into lruMemoize for a bounded cache, or if you want a component's cached results released as soon as it unmounts rather than when the argument objects are garbage collected. See "Creating Unique Selector Instances" for how to use one with useSelector, and the Reselect FAQ on sharing a selector across component instances.
Reselect 5
Reselect 5 (released December 2023) is written in TypeScript and changes a few defaults that are worth knowing about:
weakMapMemoizeis the default memoizer. As described above, it caches per distinct argument set with no size limit. To get the previous behavior, passmemoize: lruMemoize(and optionallymemoizeOptions: { maxSize: 10 }) tocreateSelectoror build a customcreateSelectorwithcreateSelectorCreator.createSelector.withTypes<RootState>()returns acreateSelectorwhose input selectors are pre-typed to receive your root state, so you do not have to annotatestatein every input selector.- Development-mode checks warn about the two common mistakes described earlier on this page: an input selector that returns a new reference on every call, and a result function that just returns its input. They run on the first call to each selector in development and are disabled in production. See Development-only checks.
import { createSelector, lruMemoize } from '@reduxjs/toolkit'
import type { RootState } from './store'
export const createAppSelector = createSelector.withTypes<RootState>()
// Input selectors receive `RootState` without annotations
export const selectCompletedTodos = createAppSelector(
[state => state.todos],
todos => todos.filter(todo => todo.completed)
)
// Opt back into a bounded LRU cache for one selector
export const selectItemsByCategory = createAppSelector(
[state => state.items, (state, category: string) => category],
(items, category) => items.filter(item => item.category === category),
{ memoize: lruMemoize, memoizeOptions: { maxSize: 10 } }
)
Redux Toolkit re-exports createSelector, createSelectorCreator, lruMemoize, and weakMapMemoize from Reselect, so you do not need to install Reselect separately. The Reselect 5 summary lists the full set of changes.
Alternative Selector Libraries
While Reselect is the most widely used selector library with Redux, there are many other libraries that solve similar problems, or expand on Reselect's capabilities.
proxy-memoize
proxy-memoize uses a different implementation approach. It relies on Proxy objects to track which nested values a selector actually reads, then compares only those values on later calls to see if they've changed. This can provide better results than Reselect in some cases.
A good example of this is a selector that derives an array of todo descriptions:
import { createSelector } from '@reduxjs/toolkit'
const selectTodoDescriptionsReselect = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(todo => todo.text)
)
Unfortunately, this will recalculate the derived array if any other value inside of state.todos changes, such as toggling a todo.completed flag. The contents of the derived array are identical, but because the input todos array changed, it has to calculate a new output array, and that has a new reference.
The same selector with proxy-memoize might look like:
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize((state: RootState) =>
state.todos.map(todo => todo.text)
)
Unlike Reselect, proxy-memoize can detect that only the todo.text fields are being accessed, and will only recalculate if one of the todo.text fields changed.
It has some tradeoffs and differences from Reselect:
- All values are passed in as a single object argument
- It's more magical, whereas Reselect is more explicit
- There are some edge cases regarding the
Proxy-based tracking behavior - It's less widely used
proxy-memoize is a reasonable alternative to Reselect if you have selectors that read only a small part of a large input and want to avoid recalculating when unrelated fields change.
re-reselect
re-reselect wraps Reselect and adds a "key selector" that picks a cache key from the selector arguments, managing a separate Reselect selector instance per key. With Reselect 5's weakMapMemoize already caching per argument set, this is mostly useful when you want an explicit key (such as a string ID) rather than reference identity to decide which cache entry to use.
import { createCachedSelector } from 're-reselect'
const selectUsersByLibrary = createCachedSelector(
// inputSelectors
selectUsers,
selectLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state: RootState, libraryName: string) => libraryName
)
Using Selectors with React-Redux
Calling Selectors with Parameters
It's common to want to pass additional arguments to a selector function. However, useSelector always calls the provided selector function with one argument - the Redux root state.
The simplest solution is to pass an anonymous selector to useSelector, and then immediately call the real selector with both state and any additional arguments:
import { selectTodoById } from './todosSlice'
import { useAppSelector } from '../../app/hooks'
function TodoListItem({ todoId }: { todoId: string }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useAppSelector(state => selectTodoById(state, todoId))
}
Creating Unique Selector Instances
A memoized selector is often shared across many components that each call it with different arguments. With Reselect 5's default weakMapMemoize, that works as-is: the shared selector keeps a cache entry per distinct argument set, so the components do not evict each other's results.
If you have opted into lruMemoize with a small cache, or want a component's cached results released as soon as it unmounts, create a unique selector instance per component with a selector factory and useMemo:
import { useMemo } from 'react'
import { makeSelectItemsByCategory } from './categoriesSlice'
import { useAppSelector } from '../../app/hooks'
function CategoryList({ category }: { category: string }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useAppSelector(state =>
selectItemsByCategory(state, category)
)
}
If you still use the legacy connect API, the equivalent is the "factory function" form of mapStateToProps, where mapState returns a new mapState function on its first call.
Using Selectors Effectively
While selectors are a common pattern in Redux applications, they are often misused or misunderstood. Here are some guidelines for using selector functions correctly.
Define Selectors Alongside Reducers
Selector functions are often defined in the UI layer, directly inside of useSelector calls. However, this means that there can be repetition between selectors defined in different files, and the functions are anonymous.
Like any other function, you can extract an anonymous function outside the component to give it a name:
const selectTodos = (state: RootState) => state.todos
function TodoList() {
const todos = useAppSelector(selectTodos)
}
However, multiple parts of the application may want to use the same lookups. Also, conceptually, we may want to keep the knowledge of how the todos state is organized as an implementation detail inside the todosSlice file, so that it's all in one place.
Because of this, it's a good idea to define reusable selectors alongside their corresponding reducers. In this case, we could export selectTodos from the todosSlice file:
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'
interface Todo {
id: string
text: string
completed: boolean
}
const initialState: Todo[] = []
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action: PayloadAction<Todo>) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = (state: RootState) => state.todos
That way, if we happen to make an update to the structure of the todos slice state, the relevant selectors are right here and can be updated at the same time, with minimal changes to any other parts of the app.
Balance Selector Usage
It's possible to add too many selectors to an application. Adding a separate selector function for every single field is not a good idea! That ends up turning Redux into something resembling a Java class with getter/setter functions for every field. It's not going to improve the code, and it's probably going to make the code worse - maintaining all those extra selectors is a lot of additional effort, and it will be harder to trace what values are being used where.
Similarly, don't make every single selector memoized!. Memoization is only needed if the selector returns a new reference every time it runs, or if the calculation logic it executes is expensive. A selector function that does a direct lookup and return of a value should be a plain function, not memoized.
Some examples of when and when not to memoize:
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = (state: RootState) => state.todos
const selectNestedValue = (state: RootState) => state.some.deeply.nested.field
const selectTodoById = (state: RootState, todoId: string) => state.todos[todoId]
// 🤔 MAYBE memoize: deriving data, but will return a consistent result.
// Memoization might be useful if the selector is used in many places
// or the list being iterated over is long.
const selectItemsTotal = (state: RootState) => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = (state: RootState) =>
state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = (state: RootState) =>
state.todos.map(todo => todo.text)
Reshape State as Needed for Components
Selectors do not have to limit themselves to direct lookups - they can perform any needed transformation logic inside. This is especially valuable to help prepare data that is needed by specific components.
A Redux state often has data in a "raw" form, because the state should be kept minimal, and many components may need to present the same data differently. You can use selectors to not only extract state, but to reshape it as needed for this specific component's needs. That could include pulling data from multiple slices of the root state, extracting specific values, merging different pieces of the data together, or any other transformations that are helpful.
It's fine if a component has some of this logic too, but it can be beneficial to pull all of this transformation logic out into separate selectors for better reuse and testability.
Globalize Selectors if Needed
There's an inherent imbalance between writing slice reducers and selectors. Slice reducers only know about their one portion of the state - to the reducer, its state is all that exists, such as the array of todos in a todoSlice. Selectors, on the other hand, usually are written to take the entire Redux root state as their argument. This means that they have to know where in the root state this slice's data is kept, such as state.todos, even though that's not really defined until the root reducer is created (typically in the app-wide store setup logic).
A typical slice file often has both of these patterns side-by-side. That's fine, especially in small or midsize apps. But, depending on your app's architecture, you may want to further abstract the selectors so that they don't know where the slice state is kept - it has to be handed to them.
We refer to this pattern as "globalizing" selectors. A "globalized" selector is one that accepts the Redux root state as an argument, and knows how to find the relevant slice of state to perform the real logic. A "localized" selector is one that expects just a piece of the state as an argument, without knowing or caring where that is in the root state:
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = (state: RootState) =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = (todos: Todo[]) =>
todos.every(todo => todo.completed)
"Localized" selectors can be turned into "globalized" selectors by wrapping them in a function that knows how to retrieve the right slice of state and pass it onwards.
Redux Toolkit's createEntityAdapter API is an example of this pattern. If you call todosAdapter.getSelectors(), with no argument, it returns a set of "localized" selectors that expect the entity slice state as their argument. If you call todosAdapter.getSelectors(state => state.todos), it returns a set of "globalized" selectors that expect to be called with the Redux root state as their argument.
There may also be other benefits to having "localized" versions of selectors as well. For example, say we have an advanced scenario of keeping multiple copies of createEntityAdapter data nested in the store, such as a chatRoomsAdapter that tracks rooms, and each room definition then has a chatMessagesAdapter state to store the messages. We can't directly look up the messages for each room - we first have to retrieve the room object, then select the messages out of that. This is easier if we have a set of "localized" selectors for the messages.
Further Information
- Selector libraries:
- Reselect
proxy-memoize: https://github.com/dai-shi/proxy-memoizere-reselect: https://github.com/toomuchdesign/re-reselect
- Randy Coulman has an excellent series of blog posts on selector architecture and different approaches for globalizing Redux selectors, with tradeoffs: