一个简单的redux的selector库
- 可以计算数据来源,允许redux只保存最小的必须数据量
- 快速高效,除非selector的参数发生变化,不然selector不会重新计算
- 可组合,可以作为别的selector的输入
Example
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非常大,或者算计算成本 很高,这种重复计算会导致一些性能问题。Reselector能够帮助避免多余的重复计算。
创建一个可以记忆的Selector
当state.todos
或者state.visibilityFilter
改变时,我们用一个带记忆功能的selector来代替getVisibleTodos
来重新计算todos
,但是state tree中不相关部分更新时,我们不会重新计算。
Reselector提供了createSelector
来创建可记忆的selector。createSelector
接收一个数组的input-selector和一个transform function作为参数,如果Redux的state tree发生改变并导致了input-selector发生变化,这个可记忆的selector就会调用transform function(以input-selector作为参数)然后返回结果。如果input-selector的数据没有变化就会返回上一次的计算值,不调用transform。
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)
}
}
)
在上面的例子中 getVisiblityFilter
和getTodos
是input-selector。他们创建了普通的不可记忆selector因为他们没有改变他们所选的state。另一方面getvisibleTodos
则是一个可记忆selector。它接收了getVisibilityFilter
和getTodos
和transform函数作为参数。
组合Selector
可记忆selector本身可以是另一个可记忆selector的参数,下面是getVisibleTodos
作为input-selector参数的例子。
const getKeyword = (state) => state.keyword
const getVisibleTodosFilteredByKeyword = createSelector(
[ getVisibleTodos, getKeyword ],
(visibleTodos, keyword) => visibleTodos.filter(
todo => todo.text.includes(keyword)
)
)
连接selector和redux store
如果你使用React Redux,你可以像平时一样在mapStateToProps
里调用selector。
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
在Selector里使用React Props
这一节在app里引入了一个假设的拓展来支持多重Todo List。要注意一个拓展的完整实现要求reducer、components、actions等的改变,但是这些不是我们要讨论的重点,为了简洁我们忽略了这部分。
目前为止我们只看到selector接受Redux store state作为参数,但是其实slector也可以接受props。
这是一个叫App
的组件,用来渲染三个VisibleTodoList
组件,其中每个组件都有一个叫listId
的prop:
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
容器应该根据listId
使用state中不同的部分,让我们来修改getVisibilityFilter
和getTodos
来接收一个props作为参数。
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
可以从mapStateToProps
的第二个参数中获取props
。
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
现在getVisibleTodos
能够使用props了,并且运行得很好。
但是有个一问题!
如果使用包含多个VisibleTodoList
容器实例的getVisibleTodos
函数不能正确的记忆state是否发生变化。
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有size为1的缓存,并且它接收的一系列参数相同时,只会返回被缓存的值。如果我们在<VisibleTodoList listId="1" />
和<VisibleTodoList listId="2" />
之间切换时,这个共用的selector会不断的接收不同的props参数({listId:1}
和{listId:2}
),这会导致selector每次都重新计算而不是返回缓存值,因为接收的props参数不同。下一节我们会解决这个问题。
共用Selector时,传入不同props依旧使用缓存的方法
下面的例子要求React Redux v4.3.0或以上
另外一个办法是re-reselect
解决办法是,每个VisibleTodoList
组件都使用专属的一个selector的copy。
我们创建一个叫makeGetVisibleTodos
的函数,每次调用时返回一个新的getVisibleTodos
的copy。
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
同时,我们需要让每个组件取使用它们专属的selector。connect
中的mapStateToProps
参数可以帮到我们。
如果connect
中的mapStateToProps
返回一个函数而不是对象,mapStateToProps
会被用于为每个组件创建一个独立的mapStateToProps
函数。
下面的例子中,makeMapStateToProps
创建了getVisibleTodos
selector,并返回了拥有独立selector的mapStateToProps
函数。
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos()
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
return mapStateToProps
}
如果我们将这个makeMapStateToProps
传给connect
,每个VisibleTodosList
组件会或者独立的mapStateToProps
和独立的getVisibleTodos
selector。记忆缓存功能现在正确的工作了。
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