Redux Toolkit: срезы, селекторы и связь с React
- Зачем это нужно
- Срез: одно описание, три готовые вещи
- Сборка хранилища и подключение к React
- Главное: что именно выбирать
- Селектор с запоминанием
- Отправка действий из компонента
- Типы: два вывода из хранилища
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Идея хранилища из прошлой главы простая, а вот записывали её раньше долго: отдельный файл с именами действий, отдельный с их создателями, отдельный с обработчиком. Отсюда и репутация подхода как многословного.
Сегодня всё это пишется одним описанием. Оно генерирует и обработчик, и создателей действий, и имена. Плюс приносит черновики, о которых уже шла речь, и разумные настройки по умолчанию.
В этой главе — как выглядит такое описание, как оно подключается к React и какая ровно одна вещь отделяет быстрый интерфейс от медленного: что именно выбирать из хранилища.
Срез: одно описание, три готовые вещи
export const vault = createSlice({
name: "vault",
initialState: { amount: "0", busy: false, ops: [] },
reducers: {
setAmount: (s, a) => { s.amount = a.payload; },
setBusy: (s, a) => { s.busy = a.payload; },
opAdded: (s, a) => { s.ops.unshift(a.payload); },
},
});
── что генерирует createSlice ──
actions.setAmount, actions.setBusy
имена действий: vault/setAmount, vault/setBusy
есть ли reducer: true
Раздел — это кусок состояния и всё, что с ним происходит. Один раздел на кошелёк, один на операции, один на форму. Не «один раздел на компонент»: разделы отражают данные, а не разметку.
Сборка хранилища и подключение к React
export const store = configureStore({
reducer: { vault: vault.reducer, wallet: wallet.reducer },
});
createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
Дальше в любом компоненте на любой глубине:
const amount = useSelector((s) => s.vault.amount);
const dispatch = useDispatch();
<button onClick={() => dispatch(vault.actions.setBusy(true))}>…</button>
Главное: что именно выбирать
Прогон ставит рядом четыре компонента с разными селекторами и меняет в хранилище две разные вещи. Считаем, кто пересчитался:
── изменили amount (его читает только первый) ──
поле 1
весь объект 1
новый массив каждый раз 1
с запоминанием 0
── изменили busy: amount и ops не трогали вовсе ──
поле 0
весь объект 1
новый массив каждый раз 1
с запоминанием 0
Разбор строк.
Выбрали поле. Пересчитался, когда изменилось именно оно, и промолчал на изменении соседнего. Это то, чего мы хотим.
Выбрали весь объект раздела. Пересчитывается на любое изменение внутри раздела — потому что объект каждый раз новый. Выигрыш, ради которого заводили хранилище, потерян.
Выбрали с фильтром прямо в селекторе. Тоже пересчитывается всегда, и по более коварной причине: фильтр создаёт новый массив при каждом вызове. Даже если данные не менялись, новый массив не равен прошлому.
Селектор с запоминанием. Ноль пересчётов в обоих случаях: операции не менялись, значит и результат тот же самый объект.
Правило: выбирайте самое узкое. Не раздел, а поле. Не объект, а нужное значение из него. Сравнение идёт по ссылке, и чем крупнее выбранное, тем чаще оно оказывается «новым».
Селектор с запоминанием
import { createSelector } from "@reduxjs/toolkit";
export const selectDeposits = createSelector(
(s) => s.vault.ops,
(ops) => ops.filter((o) => o.kind === "deposit"),
);
Когда это нужно: всегда, когда селектор что-то вычисляет — фильтрует, складывает, сортирует, собирает объект. Если селектор просто достаёт поле, запоминание не нужно: поле и так то же самое.
Отправка действий из компонента
function DepositForm() {
const amount = useSelector((s) => s.vault.amount);
const busy = useSelector((s) => s.vault.busy);
const dispatch = useDispatch();
return (
<form onSubmit={(e) => { e.preventDefault(); dispatch(deposit(amount)); }}>
<input
value={amount}
onChange={(e) => dispatch(vault.actions.setAmount(e.target.value))}
/>
<button disabled={busy}>{busy ? "Отправляем…" : "Отправить"}</button>
</form>
);
}
Обратите внимание: два отдельных обращения к хранилищу вместо одного за объектом. Так компонент пересчитается на изменение суммы и на изменение признака занятости — но не на появление новой операции в списке.
Хранить в общем хранилище то, что набирается в поле, — спорное решение: значение нужно одной форме. Здесь так сделано намеренно, чтобы показать связку; в настоящем проекте текст поля чаще оставляют в состоянии компонента, а в хранилище кладут только результат.
Типы: два вывода из хранилища
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
Два файла, четыре строки — и в компонентах перестаёт быть нужным писать тип состояния в каждом селекторе.
Типичные ошибки
Выбирают весь раздел. Компонент пересчитывается на всё подряд.
Фильтр или сортировка прямо в селекторе. Новый массив каждый раз — то же самое.
Селектор собирает объект из двух полей. s => ({ a: s.x, b: s.y }) — новый объект при каждом вызове. Либо два отдельных обращения, либо селектор с запоминанием.
Забыли обёртку хранилища в корне. Падение с сообщением о том, что обращение сделано вне провайдера.
Разделы по компонентам, а не по данным. Через неделю никто не понимает, где что лежит.
Состояние описано типом вручную. Со временем расходится с настоящим содержимым хранилища.
В хранилище кладут то, что набирается в поле. Работать будет, но каждое нажатие клавиши станет действием в общей истории.
Слова главы
- Срез — описание раздела: имя, начальное состояние, обработчики; из него получаются действия и обработчик.
- Создатель действия — функция, возвращающая объект действия; сам он ничего не делает.
- Сборка хранилища — карта «имя раздела → его обработчик».
- Обёртка хранилища — ставится в корне; без неё обращения падают.
- Селектор — функция от состояния к значению; выбирать надо самое узкое.
- Сравнение по ссылке — причина, по которой новый массив с тем же содержимым считается изменением.
- Селектор с запоминанием — возвращает прошлый результат, пока входы не изменились.
- Тип состояния из хранилища — выводится, а не пишется руками.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
export const vault = createSlice({ name: "vault", initialState, reducers: {} });
На чемпионате
Разделов два-три, по данным. Кошелёк, операции. Больше не нужно.
Селекторы — рядом со срезом, а не в компонентах. Тогда при изменении формы состояния правится одно место.
Узкие селекторы с самого начала. Переписывать потом дороже, чем написать сразу.
Четыре строки типов — в первый же час. Иначе в каждом селекторе придётся писать тип состояния руками, и однажды он разойдётся.
И практическое: на защите откройте средства отладки и покажите список действий. Читаемые имена вроде «vault/opAdded» рассказывают историю сеанса лучше, чем любые слова.
Куда углубиться
- Обработчики чужих действий — как один раздел реагирует на действие другого.
- Сущности в виде карты по идентификатору — готовое средство для списков.
- Селекторы с параметром — и почему у них своя ловушка с запоминанием.
- Сравнение по содержимому — когда сравнения по ссылке не хватает.
- Проверка на случайное изменение состояния — встроенная и включена по умолчанию в разработке.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Redux Toolkit: createSlice — описание среза целиком.
- Redux Toolkit: createSelector — запоминание и его границы.
- React Redux: хуки — обращения к хранилищу из компонентов.
- React Redux: TypeScript — те самые четыре строки типов.
- Redux Style Guide — принятые договорённости, включая именование действий.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.