README.md

    Reselect

    GitHub Workflow Status npm package Coveralls

    A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well.

    • 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 changes.
    • Selectors are composable. They can be used as input to other selectors.

    Originally inspired by getters in NuclearJS, subscriptions in re-frame and this proposal from speedskater.

    You can play around with the following example in this codepen:

    import { createSelector } from 'reselect'
    
    const selectShopItems = state => state.shop.items
    const selectTaxPercent = state => state.shop.taxPercent
    
    const selectSubtotal = createSelector(selectShopItems, items =>
      items.reduce((subtotal, item) => subtotal + item.value, 0)
    )
    
    const selectTax = createSelector(
      selectSubtotal,
      selectTaxPercent,
      (subtotal, taxPercent) => subtotal * (taxPercent / 100)
    )
    
    const selectTotal = createSelector(
      selectSubtotal,
      taxSelector,
      (subtotal, tax) => ({ total: subtotal + tax })
    )
    
    const exampleState = {
      shop: {
        taxPercent: 8,
        items: [
          { name: 'apple', value: 1.2 },
          { name: 'orange', value: 0.95 }
        ]
      }
    }
    
    console.log(selectSubtotal(exampleState)) // 2.15
    console.log(selectTax(exampleState)) // 0.172
    console.log(selectTotal(exampleState)) // { total: 2.322 }

    Table of Contents

    Installation

    npm install reselect
    
    yarn add reselect

    Usage Guide

    The Redux docs usage page on Deriving Data with Selectors covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with React-Redux.

    API

    createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)

    Takes one or more selectors, or an array of selectors, computes their values and passes them as arguments to resultFunc.

    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.

    By default, 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. This can be customized by passing a selectorOptions object with a memoizeOptions field containing options for the built-in defaultMemoize memoization function .

    const selectValue = createSelector(
      state => state.values.value1,
      state => state.values.value2,
      (value1, value2) => value1 + value2
    )
    
    // You can also pass an array of selectors
    const selectTotal = createSelector(
      [state => state.values.value1, state => state.values.value2],
      (value1, value2) => value1 + value2
    )
    
    // Selector behavior can be customized
    const customizedSelector = createSelector(
      state => state.a,
      state => state.b,
      (a, b) => a + b,
      {
        // Pass options through to the built-in `defaultMemoize` function
        memoizeOptions: {
          equalityCheck: (a, b) => a === b,
          maxSize: 10,
          resultEqualityCheck: shallowEqual
        }
      }
    )

    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 selectAB = (state, props) => state.a * props.b
    
    // props only (ignoring state argument)
    const selectC = (_, props) => props.c
    
    // state only (props argument omitted as not required)
    const selectD = state => state.d
    
    const totalSelector = createSelector(
      selectAB,
      selectC,
      selectD,
      (ab, c, d) => ({
        total: ab + c + d
      })
    )

    defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck)

    defaultMemoize memoizes the function passed in the func parameter. It is the memoize function used by createSelector.

    defaultMemoize has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size.

    defaultMemoize determines if an argument has changed by calling the equalityCheck function. As defaultMemoize is designed to be used with immutable data, the default equalityCheck function checks for changes using reference equality:

    function defaultEqualityCheck(previousVal, currentVal) {
      return currentVal === previousVal
    }

    defaultMemoize also accepts an options object as its first argument instead of equalityCheck. The options object may contain:

    interface DefaultMemoizeOptions {
      equalityCheck?: EqualityFn
      resultEqualityCheck?: EqualityFn
      maxSize?: number
    }

    Available options are:

    • equalityCheck: used to compare the individual arguments of the provided calculation function
    • resultEqualityCheck: if provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This address the common todos.map(todo => todo.id) use case, where an update to another field in the original data causes a recalculate due to changed references, but the output is still effectively the same.
    • maxSize: the cache size for the selector. If maxSize is greater than 1, the selector will use an LRU cache internally

    The returned memoized function will have a .clearCache() method attached.

    defaultMemoize can also be used with createSelectorCreator to create a new selector factory that always has the same settings for each selector.

    createSelectorCreator(memoize, ...memoizeOptions)

    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 passed 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:

    Customize equalityCheck for defaultMemoize

    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 selectSum = 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 hashFn = (...args) =>
      args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '')
    const customSelectorCreator = createSelectorCreator(memoize, hashFn)
    const selector = customSelectorCreator(
      state => state.a,
      state => state.b,
      (a, b) => {
        called++
        return a + b
      }
    )

    createStructuredSelector({inputSelectors}, selectorCreator = createSelector)

    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 input-selectors and maps them to keys in an object:

    const selectA = state => state.a
    const selectB = state => state.b
    
    // The result function in the following selector
    // is simply building an object from the input selectors
    const structuredSelector = createSelector(selectA, selectB, (a, b) => ({
      a,
      b
    }))

    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 selectA = state => state.a
    const selectB = state => state.b
    
    const structuredSelector = createStructuredSelector({
      x: selectA,
      y: selectB
    })
    
    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
      })
    })

    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 (i.e. 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 selectIsFirstTodoComplete = createSelector(
      state => state.todos[0],
      todo => todo && todo.completed
    )

    The following state update function will not work with selectIsFirstTodoComplete:

    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 selectIsFirstTodoComplete:

    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.

    Q: Why is my selector recomputing when the input state stays the same?

    A: Check that your memoization function is compatible with your state update function (i.e. 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 selectVisibleTodos = 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 selectTodos = 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 alternative 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.

    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 its arguments are immutable. See here

    Q: How do I create a selector that takes an argument?

    A: Keep in mind that selectors can access React props, so if your arguments are (or can be made available as) React props, you can use that functionality. See here for details.

    Otherwise, Reselect doesn't have built-in support for creating selectors that accepts arguments, but here are some suggestions for implementing similar functionality...

    If the argument is not dynamic you can use a factory function:

    const expensiveItemSelectorFactory = minValue => {
      return createSelector(shopItemsSelector, items =>
        items.filter(item => item.value > minValue)
      )
    }
    
    const selectSubtotal = createSelector(
      expensiveItemSelectorFactory(200),
      items => items.reduce((acc, item) => acc + item.value, 0)
    )

    The general consensus here and over at nuclear-js is that if a selector needs a dynamic argument, then that argument should probably be state in the store. If you decide that you do require a selector with a dynamic argument, then a selector that returns a memoized function may be suitable:

    import { createSelector } from 'reselect'
    import memoize from 'lodash.memoize'
    
    const expensiveSelector = createSelector(
      state => state.items,
      items => memoize(minValue => items.filter(item => item.value > minValue))
    )
    
    const expensiveFilter = expensiveSelector(state)
    
    const slightlyExpensive = expensiveFilter(100)
    const veryExpensive = expensiveFilter(1000000)

    Q: The default memoization function is no good, can I use a different one?

    A: We think it works great for a lot of use cases, but sure. See these examples.

    Q: How do I test a selector?

    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', () => {
      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 (i.e. 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', () => {
        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)
      })
    })

    Additionally, selectors keep a reference to the last result function as .resultFunc. If you have selectors composed of many other selectors this can help you test each selector without coupling all of your tests to the shape of your state.

    For example if you have a set of selectors like this:

    selectors.js

    export const selectFirst = createSelector( ... )
    export const selectSecond = createSelector( ... )
    export const selectThird = createSelector( ... )
    
    export const myComposedSelector = createSelector(
      selectFirst,
      selectSecond,
      selectThird,
      (first, second, third) => first * second < third
    )

    And then a set of unit tests like this:

    test/selectors.js

    // tests for the first three selectors...
    test("selectFirst unit test", () => { ... })
    test("selectSecond unit test", () => { ... })
    test("selectThird unit test", () => { ... })
    
    // We have already tested the previous
    // three selector outputs so we can just call `.resultFunc`
    // with the values we want to test directly:
    test("myComposedSelector unit test", () => {
      // here instead of calling selector()
      // we just call selector.resultFunc()
      assert(myComposedSelector.resultFunc(1, 2, 3), true)
      assert(myComposedSelector.resultFunc(2, 2, 1), false)
    })

    Finally, each selector has a resetRecomputations method that sets recomputations back to 0. The intended use is for a complex selector that may have many independent tests and you don't want to manually manage the computation count or create a "dummy" selector for each test.

    Q: How do I use Reselect with Immutable.js?

    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
    })
    
    // set, merge and others only return a new obj when update changes collection
    let newMap = myMap.set('a', 1)
    assert.equal(myMap, newMap)
    newMap = myMap.merge({ a: 1 })
    assert.equal(myMap, newMap)
    // map, reduce, filter and others always return a new obj
    newMap = myMap.map(a => a * 1)
    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 Immutable.is to eliminate unnecessary recomputations.

    Q: Can I share a selector across multiple component instances?

    A: Selectors created using createSelector only have a cache size of one. This can make them unsuitable for sharing across multiple instances if the arguments to the selector are different for each instance of the component. There are a couple of ways to get around this:

    • Create a factory function which returns a new selector for each instance of the component. There is built-in support for factory functions in React Redux v4.3 or higher. See here for an example.

    • Create a custom selector with a cache size greater than one.

    Q: Are there TypeScript Typings?

    A: Yes! Reselect is now written in TS itself, so they should Just Work™.

    Q: How can I make a curried selector?

    A: Try these helper functions courtesy of MattSPalmer

    Related Projects

    re-reselect

    Enhances Reselect selectors by wrapping createSelector and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function.

    Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments.

    reselect-tools

    Chrome extension and companion lib for debugging selectors.

    • Measure selector recomputations across the app and identify performance bottlenecks
    • Check selector dependencies, inputs, outputs, and recomputations at any time with the chrome extension
    • Statically export a JSON representation of your selector graph for further analysis

    reselect-debugger

    Flipper plugin and and the connect app for debugging selectors in React Native Apps.

    Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper.

    • Selectors Recomputations count in live time across the App for identify performance bottlenecks
    • Highlight most recomputed selectors
    • Dependency Graph
    • Search by Selectors Graph
    • Selectors Inputs
    • Selectors Output (In case if selector not dependent from external arguments)
    • Shows "Not Memoized (NM)" selectors

    reselect-map

    Can be useful when doing very expensive computations on elements of a collection because Reselect might not give you the granularity of caching that you need. Check out the reselect-maps README for examples.

    The optimizations in reselect-map only apply in a small number of cases. If you are unsure whether you need it, you don't!

    License

    MIT

    项目简介

    🚀 Github 镜像仓库 🚀

    源项目地址

    https://github.com/reduxjs/reselect

    发行版本 27

    v4.1.0-beta.1

    全部发行版

    贡献者 98

    全部贡献者

    开发语言

    • TypeScript 97.3 %
    • JavaScript 2.7 %