1

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)
    }
  }
)

以上示例,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
  })
)

TommY
23 声望0 粉丝

继续记录个人文章与翻译欢迎star和watch我的github issue blog:


引用和评论

0 条评论