Вычисление производных данных
Reselect — это простая библиотека для создания кешируемых и композируемых функций-селекторов. Селекторы Reselect могут использоваться для эффективного вычисления производных данных из хранилища Redux.
Мотивация для кешируемых селекторов
Давайте вернёмся к примеру списка задач:
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 вычисляется каждый раз, когда компонент обновляется. Если дерево состояния большое, или вычисление дорогое, то повторное вычисление при каждом обновлении может привести к проблемам производительности. Reselect может помочь избежать этих ненужных перевычислений.
Создание кешируемого селектора
Мы хотим заменить getVisibleTodos кешируемым селектором, который перевычисляет todos только когда изменяется значение state.todos или state.visibilityFilter, но не когда происходят изменения в других (не связанных) частях дерева состояния.
Reselect предоставляет функцию createSelector для создания кешируемых селекторов. createSelector принимает массив входных селекторов и функцию преобразования в качестве аргументов. Если дерево состояния Redux изменяется таким образом, что вызывает изменение значения входного селектора, селектор вызовет свою функцию преобразования со значениями входных селекторов в качестве аргументов и вернёт результат. Если значения входных селекторов такие же, как при предыдущем вызове селектора, он вернёт ранее вычисленное значение вместо вызова функции преобразования.
Давайте определим кешируемый селектор с именем getVisibleTodos для замены некешируемой версии выше:
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 являются входными селекторами. Они созданы как обычные некешируемые функции-селекторы, потому что они не преобразуют выбираемые данные. getVisibleTodos — это, с другой стороны, кешируемый селектор. Он принимает getVisibilityFilter и getTodos в качестве входных селекторов и функцию преобразования, которая вычисляет отфильтрованный список задач.
Компоновка селекторов
Кешируемый селектор может сам быть входным селектором для другого кешируемого селектора. Вот пример getVisibleTodos используется в качестве входного селектора для селектора, который дополнительно фильтрует задачи по ключевому слову:
const getKeyword = state => state.keyword
const getVisibleTodosFilteredByKeyword = createSelector(
[getVisibleTodos, getKeyword],
(visibleTodos, keyword) =>
visibleTodos.filter(todo => todo.text.indexOf(keyword) > -1)
)
Подключение селектора к хранилищу Redux
Если вы используете React Redux, вы можете вызывать селекторы как обычные функции внутри mapStateToProps():
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
Доступ к свойствам React в селекторах
Этот раздел представляет гипотетическое расширение нашего приложения, которое позволяет ему поддерживать несколько списков задач. Обратите внимание, что полная реализация этого расширения требует изменений в редукторах, компонентах, действиях и т. д., которые не имеют прямого отношения к обсуждаемым темам и были опущены для краткости.
До сих пор мы видели, что селекторы принимают состояние хранилища Redux в качестве аргумента, но селектор может принимать и свойства.
Вот компонент App, который отображает три компонента VisibleTodoList, каждый из которых имеет свойство 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 должен выбирать другой фрагмент состояния в зависимости от значения свойства listId, поэтому давайте изменим 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 может передаваться в getVisibleTodos из mapStateToProps:
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
Так что теперь getVisibleTodos имеет доступ к props, и всё работает нормально.
Но есть проблема!
Использование селектора getVisibleTodos с несколькими экземплярами контейнера visibleTodoList не обеспечит правильную работу кеширования:
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 возвращает кэшированное значение только тогда, когда его набор аргументов совпадает с предыдущим набором аргументов. Если мы поочерёдно отображаем <VisibleTodoList listId="1" /> и <VisibleTodoList listId="2" />, общий селектор будет чередовать получение {listId: 1} и {listId: 2} в качестве аргумента props . Это приведет к тому, что аргументы будут отличаться при каждом вызове, поэтому селектор всегда будет перевычислять, вместо того, чтобы возвращать кэшированное значение. В следующем разделе мы рассмотрим, как преодолеть это ограничение.
Обмен селекторами между несколькими компонентами
Примеры в этом разделе требуют React Redux версии 4.3.0 или выше
Чтобы разделить селектор между несколькими компонентами VisibleTodoList и сохранить кеширование, каждый экземпляр компонента должен иметь свою собственную копию селектора.
Давайте создадим функцию с именем makeGetVisibleTodos , которая возвращает новую копию селектора getVisibleTodos каждый раз при её вызове:
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
Нам также нужен способ предоставить каждому экземпляру контейнера доступ к своему собственному частному селектору. Аргумент mapStateToProps функции connect может помочь в этом.
Если аргумент mapStateToProps, переданный в connect, возвращает функцию вместо объекта, он будет использоваться для создания индивидуальной функции mapStateToProps для каждого экземпляра контейнера.
В примере ниже makeMapStateToProps создаёт новый селектор getVisibleTodos и возвращает функцию mapStateToProps, которая имеет исключительный доступ к новому селектору:
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos()
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
return mapStateToProps
}
Если мы передадим makeMapStateToProps в connect, каждый экземпляр контейнера VisibleTodosList получит свою собственную функцию mapStateToProps с частным селектором getVisibleTodos . Кеширование теперь будет работать правильно независимо от порядка рендеринга контейнеров VisibleTodoList.
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
Дальнейшие шаги
Ознакомьтесь с официальной документацией Reselect, а также с её часто задаваемыми вопросами. Большинство проектов Redux начинают использовать Reselect, когда у них возникают проблемы с производительностью из-за слишком большого количества производных вычислений и ненужных перерендерингов, поэтому убедитесь, что вы знакомы с ним, прежде чем создавать что-то большое. Также может быть полезно изучить исходный код, чтобы вы не думали, что это магия.
© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/recipes/ComputingDerivedData.html