Селекторы и библотека reselect
Селекторы – это функции, которые принимают state и достают (возвращают) из него данные, при этом могут при необходимости их обрабатывать дополнительно даже какой-то очень сложной логикой (фильтры, расчеты и т.д.). Эти функции-селекторы вызываются внутри mapStateToProps вместо обычного прямого доставания со стейта.
Reselect – это библиотека, благодаря которой селекторам не приходится постоянно перезапускать рендеринг без изменений в стейте. При первом вызове реселекта значения зависимостей (простых передаваемых внутрь селекторов) кешируются, а при следующих вызовах данные этих зависимостей сравниваются с кешироваными. Если данные простых селекторов не изменились, то сразу возвращается кеншированный результат и без лишней перерисовки компоненты, если же что-то из зависимостей изменилось в стейте по сравнению с кешем то тогда уже вызывается селектор с «дорогими» вычислениями.
Использование секлекторов
- Допустим у нас в mapStateToProps файла UsersContainer.jsx были следующие значения:
let mapStateToProps = (state) => { return { users: state.profilePage.users, pageSize:state.profilePage.pageSize, totalUsersCount: state.profilePage.totalUsersCount, currentPage: state.profilePage.currentPage, isFetching: state.profilePage.isFetching(state), followingInProgress: state.profilePage.followingInProgress } }
- Чтобы применить селекторы мы создаем файл src/redux/users-selectors.js, в котором и прописываем селекторы для каждого из этих значений:
export const getUsers = (state) => { return state.usersPage.users; } export const getPageSize = (state) => { return state.usersPage.pageSize; } export const getTotalUsersCount = (state) => { return state.usersPage.totalUsersCount; } export const getCurrentPage = (state) => { return state.usersPage.currentPage; } export const getIsFetching = (state) => { return state.usersPage.isFetching; } export const getFollowingInProgress = (state) => { return state.usersPage.followingInProgress; }
Как видим, селекторы принято называть начиная с «get». Все они экспортируются.
- Теперь все эти функции импортируем в наш файл UsersContainer.jsx и вызываем вместо прямого обращения к стейту внутриmapStateToProps:
import { getPageSize, getUsers, getTotalUsersCount, getCurrentPage, getIsFetching, getFollowingInProgress } from '../../redux/users-selectors'; ... let mapStateToProps = (state) => { return { users: getUsers(state), pageSize: getPageSize(state), totalUsersCount: getTotalUsersCount(state), currentPage: getCurrentPage(state), isFetching: getIsFetching(state), followingInProgress: getFollowingInProgress(state) } } ...
Использование библиотеки reselect
- Устанавливаем ее:
npm install reselect
либоyarn add reselect
- Внутри нашего файла users-selectors.js с селекторами импортируем createSelector:
import { createSelector } from "reselect";
- Теперь в качестве примера применим reselect для getUsers. Для этого сам наш старый простой селектор getUsers, который только получал данные, переименовываем в getUsersSelector, а затем создадим новый getUsers, который уже будет не просто селектор, а createSelector. При этом первым параметром он примет наш простой селектор getUsersSelector (называется зависимостью), а вторым – название переменной, которая будет содержать возвращаемое значение нашим простым селектором:
import { createSelector } from "reselect"; const getUsersSelector = (state) => { return state.usersPage.users; } export const getUsers = createSelector( getUsersSelector, (users) => { return users; })
На самом деле это очень простой пример. На практике, например, одни селектор может вызываться еще одним промежуточным селектором, который делает какие-либо вычесления, а тот уже потом будет передан в криэйт-селектор:
const getUsers = (state) => { return state.usersPage.users; } const getUsersSelector = (state) => { return getUsers(state).filter(u => true); } export const getUsersSuperSelector = createSelector( getUsersSelector, (users) => { return users.filter(u => true); })
Либо же мы можем подписаться сразу на несколько селекторов (создать больше зависимостей) и производить более серьезные расчеты внутри, как селектора, так и криэйт-селектора:
export const getUsers = createSelector( getUsersSelector, getIsFetching, (users) => { return users.filter(u => true); })
Зачем нужны селекторы?
На первый взгляд может показаться, что они усложняют работу. Но благодаря им при каких-либо изменениях в названии переменных внутри стейта нам не нужно будет менять доставание со стейта в разных местах кода внутри mapStateToProps. Достаточно будет внести эти изменения внутри самой функции-селектора.
Зачем нужна библиотека reselect?
Селекторы сами по себе имеют несколько недостатков. Дело в том, что функция mapStateToProps запускается постоянно при любых изменениях в стейте, при этом перерисовка без применения селекторов (при доставании данных прямо со стейта) не происходит, если ничего не поменялось из тех объектов, на которые мы подписались внутри mapStateToProps. Так и должно быть. Но вот в случае использования селекторов перерисовка происходит постоянно при каждом запуске mapStateToProps, даже если наши конкретные изначальные объекты в стейте не поменялись (точнее не создались их копии), ведь функция возвращает каждый раз новый объект.
В итоге постоянный рендеринг создает дополнительную нагрузку. Но даже это не самое страшное, особенно если селекторые простые и не производят каких-то очень сложных вычеслений. А хуже всего в этом то, что в таких условиях невозможно будет дебажить, так как дебагер будет постоянно срабатывать.
Поэтому библоитека reselect и применяется, чтобы создавать криэйт-селекторы, задавая в качестве первых параметров простой селектор (или несколько селекторов) и следя за изменением возвращаемых ими значений, сравнивая их с кэшированными. По сути происходит то же, что до этого нам делал mapStateToProps, только не с самим стейтом, а возвращаемыми селекторами значениями. В итоге reselect позволяет делать перерисовку и запускать более сложные расчеты только тогда, когда значение простого селектора изменилось. Подписываться внутри одного такого криэйт-селектора сразу можно на несколько селекторов, в результате чего перерисовка будет происходить при изменении в значения в любом из них.