Reselect README.md(zh-cn)

Reselect

Selector library for Redux

針對Redux(和其餘)的簡單"selector"庫,靈感來自NuclearJS的getters,re-frame的subscriptions和來自speedskater這篇議題html

  • Selectors能夠計算派生數據,容許Redux儲存儘量小的state。
  • Selectors是高效的。一個selector不會從新計算除非它的參數發生變化。
  • Selectors是能夠組合的。他們能夠做爲其餘selector的輸入。

你能夠在this codepen中嘗試如下例子react

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) => ({ total: subtotal + tax })
)


let exampleState = {
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
}

console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState))      // 0.172
console.log(totalSelector(exampleState))    // { total: 2.322 }

安裝

npm install reselect

例子

若是你青睞視頻教程, 你能夠看這裏.git

Memoized Selectors的動機

The examples in this section are based on the Redux Todos List example.

containers/VisibleTodoList.jsgithub

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

在以上的例子,mapStateToProps調用getVisibleTodos來計算todos。這很好,可是這裏有一個缺陷:todos在每次state tree更新後會被計算。若是state tree很是大或者高昂計算,每次更新時的重複計算可能致使性能問題。Reselect能夠幫助你避免這些不要的從新計算。npm

建立一個Memoized Selector

咱們更願意使用memoized selector替換getVisibleTodos,當state.todos或者state.visibilityFilter變化時它將從新計算todos,可是當變化發生在state tree其餘(不相關的)屬性是不會從新計算。redux

Reselect提供一個方法createSelector來建立memoized selectors。createSelector接受input-selectors數組和一個轉換函數做爲他的參數。若是Redeux state tree發生變化致使input-selectors的值改變,則選擇器將使用input-selectors的值做爲參數調用其transform函數並返回結果。數組

讓咱們來定義一個memoized selector名叫getVisibleTodos來替換以前non-memoized版本:緩存

selectors/index.jsapp

import { createSelector } from 'reselect'

const getVisibilityFilter = (state) => state.visibilityFilter
const getTodos = (state) => state.todos

export const getVisibleTodos = createSelector(
     [ getVisibilityFilter, getTodos ],
     (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  }
)

以上示例,getVisibilityFiltergetTodos是input-selectors。他們做爲non-memoized selector函數建立,由於他們不會轉換他們所選擇的數據。getVisibleTodos則是一個memoized selector。它接受getVisibilityFiltergetTodos做爲input-selectors,而且使用轉換函數去計算過濾的todos list。函數

Composing Selectors

一個 memoized selector 自身能夠做爲其餘 memoized selector的一個 input-selector。這裏getVisibleTodos被用來做爲另外一個selector的一個input-selector
用來以keyword進一步過濾數據:

const getKeyword = (state) => state.keyword

const getVisibleTodosFilteredByKeyword = createSelector(
  [ getVisibleTodos, getKeyword ],
  (visibleTodos, keyword) => visibleTodos.filter(
    todo => todo.text.includes(keyword)
  )
)

Connecting 一個 Selector 到 Redux Store

若是你正在使用 React-Redux,你能夠在mapStateToProps()內部常規調用selectors:

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { getVisibleTodos } from '../selectors' // <-

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state)  // <-
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

在Selectors中得到React Props

本節介紹咱們的應用程序的假設擴展,容許它支持多個Todo列表。請注意,此擴展的完整實現須要更改與所討論主題不直接相關的reducer,components,actions等,而且爲簡潔起見而省略。

到如今爲止咱們僅瞭解到selectors接受Redux store做爲參數,可是selectors也能接受props。

這是一個render三個VisibleTodoList組件的App組件,每個都有一個listId屬性

components/App.js

import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <VisibleTodoList listId="1" />
    <VisibleTodoList listId="2" />
    <VisibleTodoList listId="3" />
  </div>
)

每個VisibleTodoList container 能根據它的listId選取state中不一樣的片斷,因此讓咱們來調整getVisibilityFilter,getTodos`以接受一個props參數

selectors/todoSelectors.js

import { createSelector } from 'reselect'

const getVisibilityFilter = (state, props) =>
  state.todoLists[props.listId].visibilityFilter

const getTodos = (state, props) =>
  state.todoLists[props.listId].todos

const getVisibleTodos = createSelector(
  [ getVisibilityFilter, getTodos ],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_COMPLETED':
        return todos.filter(todo => todo.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(todo => !todo.completed)
      default:
        return todos
    }
  }
)

export default getVisibleTodos

props可由mapStateToProps被傳入到getVisibleTodos:

const mapStateToProps = (state, props) => {
  return {
    todos: getVisibleTodos(state, props)
  }
}

到目前爲止getVisibleTodos可訪問props,這一切看起來很ok。

可是有一個問題

VisibleTodoList container的多個實例使用了 getVisibleTodos selector 將沒法正確memoize記憶:

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { getVisibleTodos } from '../selectors'

const mapStateToProps = (state, props) => {
  return {
    // WARNING: THE FOLLOWING SELECTOR DOES NOT CORRECTLY MEMOIZE
    todos: getVisibleTodos(state, props)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

一個經過createSelector建立的selector緩存的大小爲1,僅當它的參數與先前一次參數相同時返回cache。若是咱們在<VisibleTodoList listId="1" /><VisibleTodoList listId="2" />間交替渲染,共享的selector將交替的接受{listId: 1}{listId: 2}最爲它的props參數。這將會致使每次調用時參數不一樣因此selector老是會重複計算而不是使用緩存值。咱們將在下一節看到如何解決這個侷限。

Sharing Selectors with Props Across Multiple Component Instances

此示例要求React Redux v4.3.0 或更高
一個能夠替換的方案能夠在 re-reselect中找到

爲了在多個VisibleTodoList實例共享一個selector,每個示例組件都須要擁有屬於本身的selector私拷貝。

讓咱們建立一個函數名叫makeGetVisibleTodos用來在每次調用時返回一個新的getVisibleTodos selector 拷貝

selectors/todoSelectors.js

import { createSelector } from 'reselect'

const getVisibilityFilter = (state, props) =>
  state.todoLists[props.listId].visibilityFilter

const getTodos = (state, props) =>
  state.todoLists[props.listId].todos

const makeGetVisibleTodos = () => {
  return createSelector(
    [ getVisibilityFilter, getTodos ],
    (visibilityFilter, todos) => {
      switch (visibilityFilter) {
        case 'SHOW_COMPLETED':
          return todos.filter(todo => todo.completed)
        case 'SHOW_ACTIVE':
          return todos.filter(todo => !todo.completed)
        default:
          return todos
      }
    }
  )
}

export default makeGetVisibleTodos

咱們還須要一種方法來爲容器的每一個實例提供對其本身的私有選擇器的訪問權限。 connect的mapStateToProps參數能夠幫助解決這個問題。

If the mapStateToProps argument supplied to connect returns a function instead of an object, it will be used to create an individual mapStateToProps function for each instance of the container.

如下例子中makeMapStateToProps建立了一個新的getVisibleTodos selector, 並返回了一個mapStateToProps擁有對新selector的獨佔訪問權限

const makeMapStateToProps = () => {
  const getVisibleTodos = makeGetVisibleTodos()
  const mapStateToProps = (state, props) => {
    return {
      todos: getVisibleTodos(state, props)
    }
  }
  return mapStateToProps
}

若是咱們把makeMapStateToProps傳入到connect, 沒一個VisibleTodoList container實例將會得到屬於本身的mapStateToProps方法(帶有私有getVisibleTodos selector)。這下Memoization將正常工做。

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { makeGetVisibleTodos } from '../selectors'

const makeMapStateToProps = () => {
  const getVisibleTodos = makeGetVisibleTodos()
  const mapStateToProps = (state, props) => {
    return {
      todos: getVisibleTodos(state, props)
    }
  }
  return mapStateToProps
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  makeMapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

API

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

接受一個或多個selectors,或者一個selectors數組,計算他們的值並將值看成傳輸傳入到resultFunc

createSelector經過在調用之間使用reference equality(===)決定一個來自input-selector返回的值是否發生變化。使用createSelector建立的輸入selector應該是不可變的(immutable)。

使用createSelector建立的Selectors的緩存大小爲1。這意味這他們總會從新計算當一個input-select的值改變,一個選擇器只會存儲每一個input-selector的前一次值(pre-value: 類比pre-state)

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
)

從selector中訪問組件的props可能頗有用。當一個Selector經過connectconnected到一個組件,組件的props被做爲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
  })
)
相關文章
相關標籤/搜索