Получение данных с помощью селекторов
Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
- Почему хорошая архитектура Redux минимизирует состояние и вычисляет дополнительные данные
- Принципы использования функций-селекторов для получения данных и инкапсуляции выборок
- Как использовать библиотеку Reselect для создания мемоизированных селекторов и оптимизации
- Продвинутые техники работы с Reselect
- Дополнительные инструменты и библиотеки для создания селекторов
- Лучшие практики написания селекторов
Получение данных
Мы настоятельно рекомендуем, чтобы приложения Redux хранили состояние минимальным и вычисляли дополнительные значения из этого состояния при любой возможности.
Это включает такие операции, как фильтрация списков или суммирование значений. Например, приложение для управления задачами может хранить исходный список объектов задач в состоянии, но вычислять отфильтрованный список задач вне состояния при каждом обновлении. Аналогично, проверка завершённости всех задач или подсчёт оставшихся элементов также могут вычисляться вне хранилища.
Преимущества:
-
Исходное состояние проще для восприятия
-
Требуется меньше логики для вычисления дополнительных значений и их синхронизации
-
Оригинальное состояние остаётся доступным для ссылок и не заменяется
Это также является хорошим принципом для состояния React! Часто пользователи пытаются определить хук useEffect, который ожидает изменения значения состояния, а затем сохраняет производное значение через setAllCompleted(allCompleted). Вместо этого значение можно вычислять непосредственно в процессе рендеринга и использовать напрямую, без сохранения в состояние:
function TodoList() {
const [todos, setTodos] = useState([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
Вычисление производных данных с помощью селекторов
В типичном Redux-приложении логика вычисления данных обычно реализуется через функции, которые мы называем селекторами.
Селекторы в первую очередь используются для инкапсуляции логики выборки значений из состояния, вычисления производных данных и оптимизации производительности за счёт предотвращения избыточных вычислений.
Вы не обязаны использовать селекторы для всех обращений к состоянию, но это стандартный и широко распространённый паттерн.
Основные концепции селекторов
"Функция-селектор" — это любая функция, которая принимает состояние хранилища Redux (или его часть) в качестве аргумента и возвращает данные на основе этого состояния.
Для создания селекторов не требуется специальная библиотека, и не имеет значения, объявляете ли вы их через стрелочные функции или ключевое слово function. Например, все эти варианты являются валидными функциями-селекторами:
// Arrow function, direct lookup
const selectEntities = state => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
items.filter(item => item.name.startsWith(namePrefix))
Функция-селектор может иметь любое имя. Однако мы рекомендуем начинать имя функции с префикса select и описания выбираемого значения. Типичные примеры: selectTodoById, selectFilteredTodos и selectVisibleTodos.
Если вы использовали хук useSelector из React-Redux, вы уже знакомы с базовой идеей функции-селектора — функции, которые мы передаём в useSelector, должны быть селекторами:
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}
Функции-селекторы обычно определяются в двух частях Redux-приложения:
-
В файлах слайсов (slices), вместе с логикой редюсеров
-
В файлах компонентов, либо вне компонента, либо инлайн в вызовах
useSelector
Функции-селекторы можно использовать везде, где есть доступ ко всему корневому состоянию Redux. Это включает хук useSelector, функцию mapState для connect, middleware, thunks и sagas. Например, thunks и middleware имеют доступ к аргументу getState, поэтому вы можете вызывать селекторы непосредственно там:
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
Обычно невозможно использовать селекторы внутри редьюсеров, потому что редьюсер среза имеет доступ только к своему собственному фрагменту состояния Redux, а большинство селекторов ожидают получения всего корневого состояния Redux в качестве аргумента.
Инкапсуляция структуры состояния с помощью селекторов
Первая причина использовать функции-селекторы — это инкапсуляция и повторное использование при работе со структурой состояния Redux.
Предположим, один из ваших хуков useSelector выполняет специфическое обращение к части состояния Redux:
const data = useSelector(state => state.some.deeply.nested.field)
Это корректный код, но архитектурно не оптимальный. Представьте, что у вас несколько компонентов обращаются к этому полю. Что произойдёт, если потребуется изменить местоположение этого фрагмента состояния? Придётся изменять каждый хук useSelector, ссылающийся на это значение. Поэтому, подобно тому как мы рекомендуем использовать создатели действий для инкапсуляции логики, мы советуем определять переиспользуемые селекторы, инкапсулирующие знание о местоположении данных. Затем вы сможете многократно использовать одну функцию-селектор в кодовой базе везде, где приложению нужны эти данные.
В идеале только редьюсеры и селекторы должны знать точную структуру состояния, поэтому при изменении расположения данных потребуется обновить лишь эти две части логики.
Поэтому часто рекомендуется определять переиспользуемые селекторы непосредственно в файлах срезов, а не внутри компонентов.
Распространённая аналогия: селекторы похожи на "запросы к вашему состоянию". Вам не важно, как именно запрос получил данные, важно лишь, что вы запросили их и получили результат.
Оптимизация селекторов с помощью мемоизации
Функциям-селекторам часто требуется выполнять "ресурсоёмкие" вычисления или создавать производные значения с новыми ссылками на объекты/массивы. Это может влиять на производительность по нескольким причинам:
-
Селекторы, используемые с
useSelectorилиmapState, перезапускаются после каждого действия, независимо от того, какая часть состояния изменилась. Повторение ресурсоёмких вычислений при неизменных входных данных тратит ресурсы процессора, а входные данные обычно не меняются. -
useSelectorиmapStateполагаются на проверку ссылочного равенства (===) возвращаемых значений. Если селектор всегда возвращает новые ссылки, компонент будет перерисовываться, даже если данные идентичны. Особенно актуально для операций с массивами (map(),filter()), возвращающих новые ссылки.
Например, этот компонент написан неудачно, потому что его useSelector всегда возвращает новую ссылку на массив, вызывая перерисовку после каждого действия, даже если state.todos не изменился:
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useSelector(state =>
state.todos.filter(todo => todo.completed)
)
}
Другой пример — компонент, требующий "ресурсоёмких" преобразований данных:
function ExampleComplexComponent() {
const data = useSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
Эта "ресурсоёмкая" логика будет перезапускаться после каждого действия. Это не только создаёт новые ссылки, но и выполняет ненужную работу, если state.data не изменился.
Поэтому нужен способ писать оптимизированные селекторы, избегающие пересчёта при неизменных входных данных. Здесь вступает идея мемоизации.
Мемоизация — это форма кэширования. Она подразумевает отслеживание входных данных функции и сохранение этих данных вместе с результатами для последующего использования. Если функция вызывается с теми же входными данными, что и ранее, она может пропустить выполнение фактической работы и вернуть тот же результат, который был сгенерирован при предыдущем вызове с этими данными. Это оптимизирует производительность, выполняя работу только при изменении входных данных и стабильно возвращая те же ссылки на результаты при идентичных входах.
Далее мы рассмотрим варианты написания мемоизированных селекторов.
Написание мемоизированных селекторов с помощью Reselect
Экосистема Redux традиционно использует библиотеку Reselect для создания мемоизированных функций-селекторов. Также существуют другие похожие библиотеки, вариации и обёртки вокруг Reselect — мы рассмотрим их позже.
Обзор createSelector
Reselect предоставляет функцию createSelector для генерации мемоизированных селекторов. createSelector принимает одну или несколько функций-"входных селекторов" плюс функцию-"выходной селектор", возвращая новую функцию-селектор для использования.
createSelector включена в официальный пакет Redux Toolkit и реэкспортируется для удобства.
createSelector может принимать несколько входных селекторов, которые можно передавать как отдельные аргументы или как массив. Результаты всех входных селекторов передаются как отдельные аргументы в выходной селектор:
const selectA = state => state.a
const selectB = state => state.b
const selectC = state => state.c
const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
// Call the selector function and get a result
const abc = selectABC(state)
// could also be written as separate arguments, and works exactly the same
const selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
При вызове селектора Reselect запустит ваши входные селекторы со всеми переданными аргументами и проанализирует возвращённые значения. Если какие-либо результаты === отличаются от предыдущих, он перезапустит выходной селектор, передав эти результаты в качестве аргументов. Если все результаты совпадают с предыдущими, он пропустит перезапуск выходного селектора и вернёт закэшированный финальный результат.
Это означает, что "входные селекторы" обычно должны только извлекать и возвращать значения, а "выходной селектор" — выполнять преобразования.
Распространённая ошибка — написать "входной селектор", который извлекает или вычисляет значение, и "выходной селектор", который просто возвращает результат:
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
Любой "выходной селектор", просто возвращающий входные данные, некорректен! Выходной селектор всегда должен содержать логику преобразования.
Аналогично, мемоизированный селектор никогда не должен использовать state => state как вход! Это заставит селектор постоянно пересчитываться.
В типичном использовании Reselect вы пишете верхнеуровневые "входные селекторы" как простые функции, возвращающие значения из состояния. Затем используете createSelector для создания мемоизированных селекторов, которые принимают одно или несколько этих значений и производят новые производные данные:
const selectTodos = state => state.todos.items
const selectCurrentUser = state => state.users.currentUser
const selectTodosForCurrentUser = createSelector(
[selectTodos, selectCurrentUser],
(todos, currentUser) => {
console.log('Output selector running')
return todos.filter(todo => todo.ownerId === currentUser.userId)
}
)
const todosForCurrentUser1 = selectTodosForCurrentUser(state)
// Log: "Output selector running"
const todosForCurrentUser2 = selectTodosForCurrentUser(state)
// No log output
console.log(todosForCurrentUser1 === todosForCurrentUser2)
// true
Обратите внимание, что при втором вызове selectTodosForCurrentUser "выходной селектор" не выполнялся. Поскольку результаты selectTodos и selectCurrentUser совпали с первым вызовом, selectTodosForCurrentUser смог вернуть мемоизированный результат.
Поведение createSelector
Важно отметить, что по умолчанию createSelector мемоизирует только последний набор параметров. Это означает, что при многократном вызове селектора с разными входами он всё равно вернёт результат, но будет вынужден постоянно перезапускать выходной селектор:
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
Кроме того, вы можете передавать в селектор несколько аргументов. Reselect вызовет все входные селекторы с этими точными входами:
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const item = selectItemById(state, 42)
/*
Internally, Reselect does something like this:
const firstArg = selectItems(state, 42);
const secondArg = selectItemId(state, 42);
const result = outputSelector(firstArg, secondArg);
return result;
*/
Из-за этого важно, чтобы все предоставленные "входные селекторы" принимали одинаковые типы параметров. В противном случае селекторы сломаются.
const selectItems = state => state.items
// expects a number as the second argument
const selectItemId = (state, itemId) => itemId
// expects an object as the second argument
const selectOtherField = (state, someObject) => someObject.someField
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
В этом примере selectItemId ожидает, что её вторым аргументом будет простое значение, тогда как selectOtherField предполагает, что второй аргумент — это объект. Если вызвать selectItemById(state, 42), selectOtherField завершится ошибкой, поскольку пытается обратиться к 42.someField.
Паттерны использования Reselect и ограничения
Вложенные селекторы
Селекторы, созданные с помощью createSelector, можно использовать как входные данные для других селекторов. В этом примере selectCompletedTodos выступает входным параметром для selectCompletedTodoDescriptions:
const selectTodos = state => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
Передача входных параметров
Созданный Reselect селектор может вызываться с любым количеством аргументов: selectThings(a, b, c, d, e). Однако для повторного выполнения важны не количество аргументов или изменение их ссылок, а результаты «входных селекторов» и изменение их результатов. Аналогично, аргументы «выходного селектора» зависят исключительно от возвращаемых значений входных селекторов.
Это означает, что для передачи дополнительных параметров в выходной селектор необходимо определить входные селекторы, извлекающие эти значения из исходных аргументов:
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
state => state.items,
// Take the second arg, `category`, and forward to the output selector
(state, category) => category
],
// Output selector gets (`items, category)` as args
(items, category) => items.filter(item => item.category === category)
)
Затем селектор можно использовать так:
const electronicItems = selectItemsByCategory(state, "electronics");
Для единообразия рекомендуется передавать дополнительные параметры в виде единого объекта, например selectThings(state, otherArgs), и извлекать значения из объекта otherArgs.
Фабрики селекторов
createSelector имеет размер кеша по умолчанию 1, причём для каждого уникального экземпляра селектора. Это создаёт проблемы, когда одну функцию-селектор нужно повторно использовать в разных местах с разными входными данными.
Один из вариантов — создать «фабрику селекторов»: функцию, которая вызывает createSelector() и генерирует новый уникальный экземпляр селектора при каждом вызове:
const makeSelectItemsByCategory = () => {
const selectItemsByCategory = createSelector(
[state => state.items, (state, category) => category],
(items, category) => items.filter(item => item.category === category)
)
return selectItemsByCategory
}
Это особенно полезно, когда несколько однотипных UI-компонентов должны вычислять разные подмножества данных на основе пропсов.
Альтернативные библиотеки селекторов
Хотя Reselect — самая популярная библиотека селекторов для Redux, существуют и другие решения аналогичных задач или расширяющие её возможности.
proxy-memoize
proxy-memoize — относительно новая библиотека мемоизированных селекторов, использующая уникальный подход. Она задействует ES2015 Proxy для отслеживания чтения вложенных значений, затем сравнивает только эти значения при последующих вызовах. В некоторых случаях это даёт лучшие результаты, чем Reselect.
Хороший пример — селектор, формирующий массив описаний задач:
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
К сожалению, производный массив будет пересчитываться при любом изменении state.todos, например при переключении флага todo.completed. Хотя содержимое массива идентично, из-за изменения входного массива todos создаётся новая ссылка на выходной массив.
Тот же селектор с proxy-memoize может выглядеть так:
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
В отличие от Reselect, proxy-memoize определяет, что используются только поля todo.text, и пересчитывает результат только при изменении одного из полей todo.text.
Библиотека также имеет встроенную опцию size, позволяющую задать размер кеша для экземпляра селектора.
Есть компромиссы и отличия от Reselect:
-
Все значения передаются в виде единого объекта-аргумента
-
Требуется поддержка ES2015
Proxy(не работает в IE11) -
Реализация более «магическая», тогда как Reselect более явная
-
Существуют нюансы в поведении отслеживания на основе
Proxy -
Это более новый и менее распространённый подход
Тем не менее, мы официально рекомендуем рассматривать proxy-memoize как жизнеспособную альтернативу Reselect.
re-reselect
https://github.com/toomuchdesign/re-reselect улучшает кэширование в Reselect, позволяя определять "ключевой селектор". Это помогает управлять множеством экземпляров селекторов Reselect, упрощая их использование в разных компонентах.
import { createCachedSelector } from 're-reselect'
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
)
reselect-tools
Иногда бывает сложно отследить взаимосвязи между селекторами и причины их перерасчёта. https://github.com/skortchmark9/reselect-tools предоставляет инструменты трассировки зависимостей и визуализации этих связей через DevTools.
redux-views
https://github.com/josepot/redux-views аналогичен re-reselect в части генерации уникальных ключей для кэширования. Библиотека разработана как почти полная замена Reselect и предлагалась как основа для Reselect версии 5.
Предложения для Reselect v5
Мы запустили обсуждение дорожной карты в репозитории Reselect для улучшений в будущей версии: оптимизация API для больших кэшей, переход на TypeScript и другие улучшения. Приглашаем сообщество присоединиться к обсуждению:
Reselect v5 Roadmap Discussion: Goals and API Design
Использование селекторов с React-Redux
Передача параметров в селекторы
Часто требуется передавать дополнительные аргументы в селектор. Однако useSelector всегда вызывает селектор только с одним аргументом — корневым state Redux.
Простое решение — передать в useSelector анонимный селектор, который сразу вызывает целевой селектор с state и дополнительными параметрами:
import { selectTodoById } from './todosSlice'
function TodoListitem({ todoId }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useSelector(state => selectTodoById(state, todoId))
}
Создание уникальных экземпляров селекторов
Когда один селектор используется в нескольких компонентах с разными аргументами, мемоизация ломается — селектор не видит одинаковые аргументы подряд и не может вернуть кэшированное значение.
Стандартное решение — создать уникальный экземпляр мемоизированного селектора в компоненте, а затем использовать его с useSelector. Так каждый компонент стабильно передаёт одинаковые аргументы своему экземпляру селектора, обеспечивая корректную мемоизацию.
Для функциональных компонентов это реализуется через useMemo или useCallback:
import { makeSelectItemsByCategory } from './categoriesSlice'
function CategoryList({ category }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategory(state, category)
)
}
Для классовых компонентов с connect используется фабричная функция в mapState. Если mapState при первом вызове возвращает новую функцию, она станет настоящей функцией mapState, создавая замыкание для нового экземпляра селектора:
import { makeSelectItemsByCategory } from './categoriesSlice'
const makeMapState = (state, ownProps) => {
// Closure - create a new unique selector instance here,
// and this will run once for every component instance
const selectItemsByCategory = makeSelectItemsByCategory()
const realMapState = (state, ownProps) => {
return {
itemsByCategory: selectItemsByCategory(state, ownProps.category)
}
}
// Returning a function here will tell `connect` to use it as
// `mapState` instead of the original one given to `connect`
return realMapState
}
export default connect(makeMapState)(CategoryList)
Эффективное использование селекторов
Хотя селекторы — распространённый паттерн в Redux, их часто используют неправильно. Вот ключевые рекомендации:
Определяйте селекторы вместе с редюсерами
Селекторы часто определяют в UI-слое прямо внутри useSelector. Это ведёт к дублированию кода между файлами и использованию анонимных функций.
Как любую функцию, анонимный селектор можно вынести за пределы компонента и дать ему имя:
const selectTodos = state => state.todos
function TodoList() {
const todos = useSelector(selectTodos)
}
Но разные части приложения могут использовать одинаковые выборки. Кроме того, логично хранить знания о структуре состояния todos как деталь реализации внутри файла todosSlice для централизации логики.
Поэтому рекомендуется определять переиспользуемые селекторы вместе с соответствующими редюсерами. В этом случае мы можем экспортировать selectTodos из файла todosSlice:
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = state => state.todos
Таким образом, если мы изменим структуру состояния среза todos, соответствующие селекторы будут находиться здесь же и могут быть обновлены одновременно с минимальными изменениями в других частях приложения.
Баланс в использовании селекторов
В приложение можно добавить слишком много селекторов. Создавать отдельную функцию-селектор для каждого поля — плохая идея! Это превращает Redux в нечто напоминающее Java-класс с геттерами и сеттерами для каждого поля. Это не улучшит код, а, вероятно, сделает его хуже — поддержка всех этих дополнительных селекторов потребует много усилий, а отслеживание используемых значений усложнится.
Аналогично, не следует мемоизировать каждый селектор!. Мемоизация необходима только если селектор возвращает новую ссылку при каждом вызове или если его логика вычисления ресурсоёмкая. Обычная функция-селектор, которая просто извлекает и возвращает значение, должна оставаться простой функцией без мемоизации.
Примеры, когда мемоизация нужна, а когда нет:
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = state => state.todos
const selectNestedValue = state => state.some.deeply.nested.field
const selectTodoById = (state, todoId) => state.todos[todoId]
// 🤔 MAYBE memoize: deriving data, but will return a consistent result.
// Memoization might be useful if the selector is used in many places
// or the list being iterated over is long.
const selectItemsTotal = state => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = state => state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = state => state.todos.map(todo => todo.text)
Изменение формы состояния под нужды компонентов
Селекторы не ограничиваются простым извлечением данных — они могут выполнять любую необходимую логику преобразования. Это особенно полезно для подготовки данных, необходимых конкретным компонентам.
Состояние Redux часто содержит данные в «сыром» виде, потому что оно должно быть минимальным, и многим компонентам может потребоваться представлять одни и те же данные по-разному. Вы можете использовать селекторы не только для извлечения состояния, но и для его преобразования под нужды конкретного компонента. Это может включать извлечение данных из нескольких срезов корневого состояния, выборку конкретных значений, объединение разных частей данных или любые другие полезные преобразования.
Допустимо, если компонент содержит часть этой логики, но вынесение всей логики преобразования в отдельные селекторы может быть полезным для лучшей переиспользуемости и тестируемости.
Глобализация селекторов при необходимости
Существует внутренний дисбаланс между написанием редюсеров для срезов и селекторов. Редюсеры срезов знают только о своей части состояния — для редюсера его state это всё, что существует, например, массив задач в todoSlice. Селекторы же, как правило, принимают всё корневое состояние Redux в качестве аргумента. Это означает, что они должны знать, где в корневом состоянии хранятся данные этого среза, например state.todos, хотя это место определяется только при создании корневого редюсера (обычно в логике настройки хранилища на уровне приложения).
В типичном файле среза часто присутствуют оба шаблона. Это нормально, особенно в небольших и средних приложениях. Однако, в зависимости от архитектуры приложения, может потребоваться дополнительно абстрагировать селекторы так, чтобы они не знали, где хранится состояние среза — оно должно передаваться им.
Мы называем этот шаблон «глобализацией» селекторов. «Глобализированный» селектор принимает корневое состояние Redux в качестве аргумента и знает, как найти соответствующий срез состояния для выполнения основной логики. «Локализованный» селектор ожидает только часть состояния в качестве аргумента, не зная и не заботясь о том, где она находится в корневом состоянии:
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = state =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = todos =>
todos.every(todo => todo.completed)
«Локализованные» селекторы можно превратить в «глобализированные», обернув их в функцию, которая знает, как извлечь нужный срез состояния и передать его дальше.
API createEntityAdapter из Redux Toolkit демонстрирует этот подход. Вызов todosAdapter.getSelectors() без аргументов возвращает набор "локальных" селекторов, ожидающих состояние среза сущностей. Вызов todosAdapter.getSelectors(state => state.todos) возвращает "глобальные" селекторы, ожидающие корневое состояние Redux.
"Локальные" версии селекторов имеют дополнительные преимущества. Например, при вложенном хранении нескольких копий данных createEntityAdapter (как chatRoomsAdapter для комнат, где каждая комната содержит состояние chatMessagesAdapter для сообщений), нельзя напрямую получить сообщения комнаты — сначала нужно получить объект комнаты, затем извлечь сообщения. "Локальные" селекторы упрощают этот процесс.
Дополнительные материалы
-
Библиотеки селекторов:
- Reselect: https://github.com/reduxjs/reselect
proxy-memoize: https://github.com/dai-shi/proxy-memoizere-reselect: https://github.com/toomuchdesign/re-reselectreselect-tools: https://github.com/skortchmark9/reselect-toolsredux-views: https://github.com/josepot/redux-views
-
Рэнди Кулман опубликовал отличную серию статей об архитектуре селекторов и подходах к глобализации селекторов Redux с анализом компромиссов: