Simple "selector" library for Redux inspired by getters in NuclearJS, subscriptions in re-frame and this proposal from speedskater.
- Selectors can compute derived data, allowing Redux to store the minimal possible state.
- Selectors are efficient. A selector is not recomputed unless one of its arguments change.
- Selectors are composable. They can be used as input to other selectors.
import { createSelector } from 'reselect';
const shopItemsSelector = state => state.shop.items;
const taxPercentSelector = state => state.shop.taxPercent;
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((acc, item) => acc + item.value, 0)
);
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
);
export const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => { return {total: subtotal + tax}}
);
- Installation
- Example
- API
- FAQ
- Why isn't my selector recomputing when the input state changes?
- Why is my selector recomputing when the input state stays the same?
- Can I use Reselect without Redux?
- The default memoization function is no good, can I use a different one?
- How do I test a selector?
- How do I create a selector that takes an argument?
- How do I use Reselect with Immutable.js?
- License
npm install reselect
The examples in this section are based on the Redux Todos List example.
Consider the following code:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
);
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
};
}
// Wrap the component to inject dispatch and state into it
export default connect(select)(App);
In the above example, select
calls selectTodos
to calculate visibleTodos
. This works great, but there is a drawback: visibleTodos
is calculated every time the component is updated. If the state tree is large, or the calculation expensive, repeating the calculation on every update may cause performance problems. Reselect can help to avoid these unnecessary recalculations.
We would like to replace select
with a memoized selector that recalculates visibleTodos
when the value of state.todos
or state.visibilityFilter
changes, but not when changes occur in other (unrelated) parts of the state tree.
Reselect provides a function createSelector
for creating memoized selectors. createSelector
takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is mutated in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.
Let's define a memoized selector named visibleTodosSelector
to replace select
:
import { createSelector } from 'reselect';
import { VisibilityFilters } from '../actions';
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
/*
* Definition of input-selectors.
* Input-selectors should be used to abstract away the structure
* of the store in cases where no calculations are needed
* and memoization wouldn't provide any benefits.
*/
const visibilityFilterSelector = state => state.visibilityFilter;
const todosSelector = state => state.todos;
/*
* Definition of combined-selector.
* In visibleTodosSelector, input-selectors are combined to derive new information.
* To prevent expensive recalculation of the input-selectors memoization is applied.
* Hence, these selectors are only recomputed when the value of their input-selectors change.
* If none of the input-selectors return a new value, the previously computed value is returned.
*/
export const visibleTodosSelector = createSelector(
visibilityFilterSelector,
todosSelector,
(visibilityFilter, todos) => {
return {
visibleTodos: selectTodos(todos, visibilityFilter),
visibilityFilter
};
}
);
In the example above, visibilityFilterSelector
and todosSelector
are input-selectors. They are created as ordinary non-memoized selector functions because they do not transform the data they select. visibleTodosSelector
on the other hand is a memoized selector. It takes visibilityFilterSelector
and todosSelector
as input-selectors, and a transform function that calculates the filtered todos list.
A memoized selector can itself be an input-selector to another memoized selector. Here is visibleTodosSelector
being used as an input-selector to a selector that further filters the todos by keyword:
const keywordSelector = state => state.keyword;
const keywordFilterSelector = createSelector(
[visibleTodosSelector, keywordSelector],
(visibleTodos, keyword) => visibleTodos.filter(
todo => todo.indexOf(keyword) > -1
)
);
If you are using React Redux, you connect a memoized selector to the Redux store using connect
:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
/*
* Import the selector defined in ../selectors/todoSelectors.js.
* This allows you to separate your components from the structure of your stores.
*/
import { visibleTodosSelector } from '../selectors/todoSelectors';
class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
);
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
/*
* Connect visibleTodosSelector to the App component.
* The keys of the selector result are available on the props object for App.
* In our example there is the 'visibleTodos' key which is bound to this.props.visibleTodos
*/
export default connect(visibleTodosSelector)(App);
So far we have only seen selectors receive the Redux store state as input, but it is also possible for a selector to receive the props of a component wrapped by connect
.
Consider the following example:
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';
let store = createStore(todoApp);
let rootElement = document.getElementById('root');
React.render(
<Provider store={store}>
{() => <App maxTodos={5}/>}
</Provider>,
rootElement
);
We have introduced a prop named maxTodos
to the App
component. We would like to access maxTodos
in visibleTodosSelector
so we can make sure that we do not return more Todos than it specifies. To achieve this we can make the following changes to selectors/todoSelectors.js
:
import { createSelector } from 'reselect';
import { VisibilityFilters } from './actions';
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
const visibilityFilterSelector = state => state.visibilityFilter;
const todosSelector = state => state.todos;
const maxTodosSelector = (state, props) => props.maxTodos;
export const visibleTodosSelector = createSelector(
visibilityFilterSelector,
todosSelector,
maxTodosSelector,
(visibilityFilter, todos, maxTodos) => {
const visibleTodos = selectTodos(todos, visibilityFilter).slice(0, maxTodos);
return {
visibleTodos,
visibilityFilter
};
}
);
When a selector is connected to a component with connect
, the component props are passed as the second argument to the selector. In visibleTodosSelector
we have added a new input-selector named maxTodosSelector
, which returns the maxTodos
property from its props argument.
Takes one or more selectors whose values are computed and passed as arguments to resultFn
.
createSelector
determines if the value returned by an input-selector has changed between calls using reference equality (===
). Inputs to selectors created with createSelector
should be immutable.
Selectors created with createSelector
have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.
const mySelector = createSelector(
state => state.values.value1,
state => state.values.value2,
(value1, value2) => value1 + value2
);
// You can also pass an array of selectors
const totalSelector = createSelector(
[
state => state.values.value1,
state => state.values.value2
],
(value1, value2) => value1 + value2
);
It can be useful to access the props of a component from within a selector. When a selector is connected to a component with connect
, the component props are passed as the second argument to the selector:
const abSelector = (state, props) => state.a * props.b;
// props only (ignoring state argument)
const cSelector = (_, props) => props.c;
// state only (props argument omitted as not required)
const dSelector = state => state.d;
const totalSelector = createSelector(
abSelector,
cSelector,
dSelector,
(ab, c, d) => ({
total: ab + c + d
})
);
defaultMemoize
memoizes the function passed in the func parameter. It is the memoize function used by createSelector
.
defaultMemoize
has a cache size of 1. This means it always recalculates when the value of an argument changes.
defaultMemoize
determines if an argument has changed by calling the equalityCheck
function. As defaultMemoize
is designed to be used wiht immutable data, the default equalityCheck
function checks for changes using reference equality:
function defaultEqualityCheck(currentVal, previousVal) {
return currentVal === previousVal;
}
defaultMemoize
can be used with createSelectorCreator
to customize the equalityCheck
function.
createSelectorCreator
can be used to make a customized version of createSelector
.
The memoize
argument is a memoization function to replace defaultMemoize
.
The ...memoizeOptions
rest parameters are zero or more configuration options to be passsed to memoizeFunc
. The selectors resultFunc
is passed as the first argument to memoize
and the memoizeOptions
are passed as the second argument onwards:
const customSelectorCreator = createSelectorCreator(
customMemoize, // function to be used to memoize resultFunc
option1, // option1 will be passed as second argument to customMemoize
option2, // option2 will be passed as third argument to customMemoize
option3 // option3 will be passed as fourth argument to customMemoize
);
const customSelector = customSelectorCreator(
input1,
input2,
resultFunc // resultFunc will be passed as first argument to customMemoize
);
Internally customSelector
calls the memoize function as follows:
customMemoize(resultFunc, option1, option2, option3);
Here are some examples of how you might use createSelectorCreator
:
import { createSelectorCreator, defaultMemoize } from 'reselect';
import isEqual from 'lodash.isEqual';
// create a "selector creator" that uses lodash.isEqual instead of ===
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
);
// use the new "selector creator" to create a selector
const mySelector = createDeepEqualSelector(
state => state.values.filter(val => val < 5),
values => values.reduce((acc, val) => acc + val, 0)
);
import { createSelectorCreator } from 'reselect';
import memoize from 'lodash.memoize';
let called = 0;
const customSelectorCreator = createSelectorCreator(memoize, JSON.stringify);
const selector = customSelectorCreator(
state => state.a,
state => state.b,
(a, b) => {
called++;
return a + b;
}
);
createStructuredSelector
is a convenience function for a common pattern that arises when using Reselect. The selector passed to a connect
decorator often just takes the values of its intput-selectors and maps them to keys in an object:
const mySelectorA = state => state.a;
const mySelectorB = state => state.b;
// The result function in the following selector
// is simply building an object from the input selectors
const structuredSelector = createSelector(
mySelectorA,
mySelectorB,
mySelectorC,
(a, b, c) => ({
a,
b,
c
})
);
createStructuredSelector
takes an object whose properties are input-selectors and returns a structured selector. The structured selector returns an object with the same keys as the inputSelectors
argument, but with the selectors replaced with their values.
const mySelectorA = state => state.a;
const mySelectorB = state => state.b;
const structuredSelector = createStructuredSelector({
x: mySelectorA,
y: mySelectorB
});
const result = structuredSelector({a: 1, b: 2}); // will produce {x: 1, y: 2}
Structured selectors can be nested:
const nestedSelector = createStructuredSelector({
subA: createStructuredSelector({
selectorA,
selectorB
}),
subB: createStructuredSelector({
selectorC,
selectorD
})
});
A: Check that your memoization function is compatible with your state update function (ie the reducer if you are using Redux). For example, a selector created with createSelector
will not work with a state update function that mutates an existing object instead of creating a new one each time. createSelector
uses an identity check (===
) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using Redux, mutating the state object is almost certainly a mistake.
The following example defines a simple selector that determines if the first todo item in an array of todos has been completed:
const isFirstTodoCompleteSelector = createSelector(
state => state.todos[0],
todo => todo && todo.completed
);
The following state update function will not work with isFirstTodoCompleteSelector
:
export default function todos(state = initialState, action) {
switch (action.type) {
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed);
// BAD: mutating an existing object
return state.map(todo => {
todo.completed = !areAllMarked;
return todo;
});
default:
return state;
}
}
The following state update function will work with isFirstTodoCompleteSelector
:
export default function todos(state = initialState, action) {
switch (action.type) {
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed);
// GOOD: returning a new object each time with Object.assign
return state.map(todo => Object.assign({}, todo, {
completed: !areAllMarked
}));
default:
return state;
}
}
If you are not using Redux and have a requirement to work with mutable data, you can use createSelectorCreator
to replace the default memoization function and/or use a different equality check function. See here and here for examples.
A: Check that your memoization function is compatible with your state update function (ie the reducer if you are using Redux). For example, a selector created with createSelector
that recomputes unexpectedly may be receiving a new object on each update whether the values it contains have changed or not. createSelector
uses an identity check (===
) to detect that an input has changed, so returning a new object on each update means that the selector will recompute on each update.
import { REMOVE_OLD } from '../constants/ActionTypes';
const initialState = [{
text: 'Use Redux',
completed: false,
id: 0,
timestamp: Date.now()
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case REMOVE_OLD:
return state.filter(todo => {
return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now();
});
default:
return state;
}
}
The following selector is going to recompute every time REMOVE_OLD is invoked because Array.filter always returns a new object. However, in the majority of cases the REMOVE_OLD action will not change the list of todos so the recomputation is unnecessary.
import { createselector } from 'reselect';
const todosSelector = state => state.todos;
export const visibletodosselector = createselector(
todosselector,
(todos) => {
...
}
);
You can eliminate unnecessary recomputations by returning a new object from the state update function only when a deep equality check has found that the list of todos has actually changed:
import { REMOVE_OLD } from '../constants/ActionTypes';
import isEqual from 'lodash.isEqual';
const initialState = [{
text: 'Use Redux',
completed: false,
id: 0,
timestamp: Date.now()
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case REMOVE_OLD:
const updatedState = state.filter(todo => {
return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now();
});
return isEqual(updatedState, state) ? state : updatedState;
default:
return state;
}
}
Alternatively, the default equalityCheck
function in the selector can be replaced by a deep equality check:
import { createSelectorCreator, defaultMemoize } from 'reselect';
import isEqual from 'lodash.isEqual';
const todosSelector = state => state.todos;
// create a "selector creator" that uses lodash.isEqual instead of ===
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
);
// use the new "selector creator" to create a selector
const mySelector = createDeepEqualSelector(
todosSelector,
(todos) => {
...
}
);
Always check that the cost of an alernative equalityCheck
function or deep equality check in the state update function is not greater than the cost of recomputing every time. If recomputing every time does work out to be the cheaper option, it may be that for this case Reselect is not giving you any benefit over passing a plain mapStateToProps
function to connect
.
A: Yes. Reselect has no dependencies on any other package, so although it was designed to be used with Redux it can be used independently. It is currently being used successfully in traditional Flux apps.
If you create selectors using
createSelector
make sure the objects in your store are immutable. See here
A: Creating a factory function may be helpful:
const expensiveItemSelectorFactory = minValue => {
return createSelector(
shopItemsSelector,
items => items.filter(item => item.value < minValue)
);
}
const subtotalSelector = createSelector(
expensiveItemSelectorFactory(200),
items => items.reduce((acc, item) => acc + item.value, 0)
);
A: We think it works great for a lot of use cases, but sure. See these examples.
A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test.
const selector = createSelector(
state => state.a,
state => state.b,
(a, b) => ({
c: a * 2,
d: b * 3
})
);
test("selector unit test", function() {
assert.deepEqual(selector({a: 1, b: 2}), {c: 2, d: 6});
assert.deepEqual(selector({a: 2, b: 3}), {c: 4, d: 9});
});
It may also be useful to check that the memoization function for a selector works correctly with the state update function (ie the reducer if you are using Redux). Each selector has a recomputations
method that will return the number of times it has been recomputed:
suite('selector', () => {
let state = {a: 1, b: 2};
const reducer = (state, action) => (
{
a: action(state.a),
b: action(state.b)
}
);
const selector = createSelector(
state => state.a,
state => state.b,
(a, b) => ({
c: a * 2,
d: b * 3
})
);
const plusOne = x => x + 1;
const id = x => x;
test("selector unit test", function() {
state = reducer(state, plusOne);
assert.deepEqual(selector(state), {c: 4, d: 9});
state = reducer(state, id);
assert.deepEqual(selector(state), {c: 4, d: 9});
assert.equal(selector.recomputations(), 1);
state = reducer(state, plusOne);
assert.deepEqual(selector(state), {c: 6, d: 12});
assert.equal(selector.recomputations(), 2);
});
});
A: Selectors created with createSelector
should work just fine with Immutable.js data structures.
If your selector is recomputing and you don't think the state has changed, make sure you are aware of which Immutable.js update methods always return a new object and which update methods only return a new object when the collection actually changes.
import Immutable from 'immutable';
let myMap = Immutable.Map({
a: 1,
b: 2,
c: 3
});
let newMap = myMap.set('a', 1); // set, merge and others only return a new obj when update changes collection
assert.equal(myMap, newMap);
newMap = myMap.merge({'a', 1});
assert.equal(myMap, newMap);
newMap = myMap.map(a => a * 1); // map, reduce, filter and others always return a new obj
assert.notEqual(myMap, newMap);
If a selector's input is updated by an operation that always returns a new object, it may be performing unnecessary recomputations. See here for a discussion on the pros and cons of using a deep equality check like Immmutable.is
to eliminate unnecessary recomputations.
MIT