JSPM

  • Created
  • Published
  • Downloads 11475815
  • Score
    100M100P100Q201274F
  • License MIT

Selectors for Redux Flux.

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

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