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?
- The default memoization cache size of 1 is no good, can I increase it?
- 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
};
/*
* Connet 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 state from the Redux store as input, but it is also possible for a selector to receive the props of the 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 to 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 the example above, we added a new input-selector named maxTodosSelector
which returns the maxTodos
property from its props argument. maxTodosSelector
was then added as an input-selector to visibleTodosSelector
, making maxTodos
available to the result function.
Takes a variable number or array of 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
and is designed to work with immutable data.
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. The default equalityCheck
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 then passed from 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 to 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;
}
);
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 funtion 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 whether the values it contains have updated or not. As createSelector
uses an identity check (===
) to detect that an input has changed, the selector will always recompute.
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 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 a deep equality check in the state update function is not greater than the cost of recomputing every time. Furthermore, if recomputing every time is the better option, you should think about whether Reselect is 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 this example.
A: We think it works great for a lot of use cases, but sure. Check out this example.
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