Reselect
Selector library for Redux
针对Redux(和其他)的简单"selector"库,灵感来自NuclearJS的getters,re-frame的subscriptions和来自speedskater这篇议题。
- Selectors可以计算派生数据,允许Redux储存尽可能小的state。
- Selectors是高效的。一个selector不会重新计算除非它的参数发生变化。
- Selectors是可以组合的。他们可以作为其他selector的输入。
你可以在this codepen中尝试以下例子
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
例子
如果你青睐视频教程, 你可以看这里.
Memoized Selectors的动机
The examples in this section are based on the Redux Todos List example.
containers/VisibleTodoList.js
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可以帮助你避免这些不要的重新计算。
创建一个Memoized Selector
我们更愿意使用memoized selector替换getVisibleTodos
,当state.todos
或者state.visibilityFilter
变化时它将重新计算todos
,但是当变化发生在state tree其他(不相关的)属性是不会重新计算。
Reselect提供一个方法createSelector
来创建memoized selectors。createSelector
接受input-selectors数组和一个转换函数作为他的参数。如果Redeux state tree发生变化导致input-selectors的值改变,则选择器将使用input-selectors的值作为参数调用其transform函数并返回结果。
让我们来定义一个memoized selector名叫getVisibleTodos
来替换之前non-memoized版本:
selectors/index.js
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。
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通过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
})
)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。