Selector library for Redux
針對Redux(和其餘)的簡單"selector"庫,靈感來自NuclearJS的getters,re-frame的subscriptions和來自speedskater這篇議題。html
你能夠在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
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替換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) } } )
以上示例,getVisibilityFilter
和getTodos
是input-selectors。他們做爲non-memoized selector函數建立,由於他們不會轉換他們所選擇的數據。getVisibleTodos
則是一個memoized selector。它接受getVisibilityFilter
和getTodos
做爲input-selectors,而且使用轉換函數去計算過濾的todos list。函數
一個 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) ) )
若是你正在使用 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
本節介紹咱們的應用程序的假設擴展,容許它支持多個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老是會重複計算而不是使用緩存值。咱們將在下一節看到如何解決這個侷限。
此示例要求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
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經過connect
connected到一個組件,組件的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 }) )