Reselect

一个简单的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)
    }
  }
)

在上面的例子中 getVisiblityFiltergetTodos是input-selector。他们创建了普通的不可记忆selector因为他们没有改变他们所选的state。另一方面getvisibleTodos则是一个可记忆selector。它接收了getVisibilityFiltergetTodos和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 = () => (
  
)

每个visibleTodoList容器应该根据listId使用state中不同的部分,让我们来修改getVisibilityFiltergetTodos来接收一个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的缓存,并且它接收的一系列参数相同时,只会返回被缓存的值。如果我们在之间切换时,这个共用的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创建了getVisibleTodosselector,并返回了拥有独立selector的mapStateToProps函数。

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

如果我们将这个makeMapStateToProps传给connect,每个VisibleTodosList组件会或者独立的mapStateToProps和独立的getVisibleTodosselector。记忆缓存功能现在正确的工作了。

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

你可能感兴趣的:(Reselect)