Package Exports
- reselect
- reselect/lib/index
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (reselect) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Reselect
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}}
);
Table of Contents
- 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 rubbish, can I use a different one?
- The default memoization cache size of 1 is rubbish, 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
Installation
npm install reselect
Example
Motivation for Memoized Selectors
Here is an excerpt from the Redux Todos List example:
containers/App.js
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.
Creating a Memoized Selector
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
:
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;
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.
Composing Selectors
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
)
);
Connecting a Selector to the Redux Store
If you are using React Redux, you connect a memoized selector to the Redux store using connect
:
containers/TodoApp.js
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 { visibleTodosSelector } from '../selectors/todoSelectors.js';
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
};
// Pass the selector to the connect component
export default connect(visibleTodosSelector)(App);
Accessing React Props in Selectors
It can be convenient to access props from a selector. In the following we extend the Todo List example to support multiple users. We would like to display the current user on the TodoApp.js
screen. We will use React Router and pass the user in the URL params for TodoApp.js
.
containers/Root.js
import React, { Component, PropTypes } from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import TodoApp from './TodoApp';
import Users from './Users';
import rootReducer from '../reducers';
const store = createStore(rootReducer);
export default class Root extends Component {
render() {
return (
<div>
<Provider store={store}>
{() =>
<Router history={this.props.history}>
<Route path="/users" component={Users} />
<Route path="/app/:user" component={TodoApp} />
</Router>
}
</Provider>
</div>
);
}
}
Root.propTypes = {
history: PropTypes.object.isRequired,
};
A user field is added to visibleTodosSelector
which is collected from the params prop.
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;
// ownProps is passed as second parameter to selector dependencies
const userFromPropsSelector = (_, ownProps) => ownProps.params.user,
export const visibleTodosSelector = createSelector(
visibilityFilterSelector,
todosSelector,
userFromPropsSelector, // pass as a normal selector dependency
(visibilityFilter, todos, user) => {
return {
visibleTodos: selectTodos(todos, visibilityFilter),
visibilityFilter,
user
};
}
);
A change is made to TodoApp.js to get the user from the props and display it on the screen.
containers/TodoApp.js
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 { visibleTodosSelector } from '../selectors/todoSelectors.js';
class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter, user } = this.props;
return (
<div>
<div>Current User: {user}</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
};
// Pass the selector to the connect component
export default connect(visibleTodosSelector)(App);
API
createSelector(...inputSelectors, resultFn)
createSelector([inputSelectors], resultFn)
Takes a variable number or array of selectors whose values are computed and passed as arguments to resultFn
.
createSelector
has been designed to work with immutable data.
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
);
// A selector's dependencies also receive props when using React Redux's connect decorator
const selectorWithProps = createSelector(
state => state.values.value,
(state, props) => props.value,
(valueFromState, valueFromProps) => valueFromState + valueFromProps;
);
defaultMemoizeFunc(func, valueEquals = defaultValueEquals)
defaultMemoizeFunc
memoizes the function passed in the func parameter.
defaultMemoizeFunc
(and by extension createSelector
) has been designed to work with immutable data.
defaultMemoizeFunc
determines if an argument has changed by calling the valueEquals function. The valueEquals
function is configurable. By default it checks for changes using reference equality:
function defaultValueEquals(currentVal, previousVal) {
return currentVal === previousVal;
}
defaultMemoizeFunc
has a cache size of 1. This means it always recalculates when an argument changes, as it only stores the result for preceding value of the argument.
createSelectorCreator(memoizeFunc, ...memoizeOptions)
createSelectorCreator
can be used to make a custom createSelector
.
memoizeFunc
is a a memoization function to replace defaultMemoizeFunc
.
...memoizeOptions
is a variadic number of configuration options that will be passsed to memoizeFunc
inside createSelectorSelector
:
memoizedResultFunc = memoizeFunc(funcToMemoize, ...memoizeOptions);
Here are some example of using createSelectorCreator
:
Customize valueEquals
for defaultMemoizeFunc
import { createSelectorCreator, defaultMemoizeFunc } from 'reselect';
import { isEqual } from 'lodash';
// create a "selector creator" that uses lodash.isEqual instead of ===
const createDeepEqualSelector = createSelectorCreator(
defaultMemoizeFunc,
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)
);
Use memoize function from lodash for an unbounded cache
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;
}
);
assert.equal(selector({a: 1, b: 2}), 3);
assert.equal(selector({a: 1, b: 2}), 3);
assert.equal(called, 1);
assert.equal(selector({a: 2, b: 3}), 5);
assert.equal(called, 2);
FAQ
Q: Why isn't my selector recomputing when the input state changes?
A: Check that your memoization function is compatible with your state update function (ie the reducer in 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 (see here). As createSelector
uses ===
to check if an input has changed, the selector will never recompute because the identity of the object never changes. Note that if you are using Redux, mutating the state object is highly discouraged and almost certainly a mistake.
The following example will not work with a selector created with createSelector
:
import { ADD_TODO } from '../constants/ActionTypes';
const initialState = [{
text: 'Use Redux',
completed: false,
id: 0
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
// BAD: mutating an existing object
return state.unshift(
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text
};
default:
return state;
}
}
The following example will work with a selector created with createSelector
:
import { ADD_TODO } from '../constants/ActionTypes';
const initialState = [{
text: 'Use Redux',
completed: false,
id: 0
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
// GOOD: returning a new array each time
return [{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text
}, ...state];
default:
return state;
}
}
Q: Why is my selector recomputing when the input state stays the same?
A: Check that your memoization funtion is compatible with your state update function (ie the reducer in 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 ===
to check if 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 isn't going to 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';
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 valueEquals
function in the selector can be replaced by a deep equality check:
import { createSelectorCreator, defaultMemoizeFunc } from 'reselect';
import { isEqual } from 'lodash';
const todosSelector = state => state.todos;
// create a "selector creator" that uses lodash.isEqual instead of ===
const createDeepEqualSelector = createSelectorCreator(
defaultMemoizeFunc,
isEqual
);
// use the new "selector creator" to create a selector
const mySelector = createDeepEqualSelector(
todosSelector,
(todos) => {
...
}
);
Always check that the cost of an alernative valueEquals
function or a deep equals 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, also consider whether Reselect is giving you any benefit over passing a plain mapStateToProps
function to connect
.
Q: Can I use Reselect without Redux?
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
How do I create a selector that takes an argument?
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)
);
Q: The default memoization function is rubbish, can I use a different one?
Sure. See this example.
Q: The default memoization cache size of 1 is rubbish, can I increase it?
You can. Check out this example.
How do I test a selector?
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 in Redux). Each selector has a method recomputations
that will return the number of times it has been recomputed. This method can be used to check if a state update required the selector to recompute.
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);
});
});
How do I use Reselect with Immutable.js?
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 update actually changes the collection.
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
as the valueEquals
function for a selector.
License
MIT