Перейти к основному содержимому
Блокчейн49ГЛАВА 49Redux Toolkit: срезы, селекторы и связь с Reactslicenamestatereducersreduceractionsvault/setuseSelectors.vault.amount

Redux Toolkit: срезы, селекторы и связь с React

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Идея хранилища из прошлой главы простая, а вот записывали её раньше долго: отдельный файл с именами действий, отдельный с их создателями, отдельный с обработчиком. Отсюда и репутация подхода как многословного.

Сегодня всё это пишется одним описанием. Оно генерирует и обработчик, и создателей действий, и имена. Плюс приносит черновики, о которых уже шла речь, и разумные настройки по умолчанию.

В этой главе — как выглядит такое описание, как оно подключается к 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
ОДНО ОПИСАНИЕ — ТРИ ГОТОВЫЕ ВЕЩИcreateSlice({ name: "vault", initialState, reducers: { setAmount, setBusy }})slice.reducerобработчик для хранилищаslice.actions.setAmountсоздатель действия"vault/setAmount"имя действия — самоимена действий складываются из названия среза и названия обработчика — придумывать их не надораньше всё это писали руками в трёх файлах; отсюда репутация подхода как многословного
Из одного описания получаются обработчик, создатели действий и их имена
Нажми на часть выражения

Раздел — это кусок состояния и всё, что с ним происходит. Один раздел на кошелёк, один на операции, один на форму. Не «один раздел на компонент»: разделы отражают данные, а не разметку.

Сборка хранилища и подключение к 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
ЧЕТЫРЕ СЕЛЕКТОРА, ДВА ИЗМЕНЕНИЯ: КТО ПЕРЕСЧИТАЛСЯизменили amountизменили busyвыводs.vault.amount10то, что надоs.vault11лишняя работаs.vault.ops.filter(…)11каждый раз новый массивселектор с запоминанием00ops не менялись — пересчёта нетвыбирать надо самое узкое: компонент пересчитывается тогда, когда выбранное значение стало другим
Изменили одно поле — пересчитались трое из четырёх; один не пересчитался ни разу

Разбор строк.

Выбрали поле. Пересчитался, когда изменилось именно оно, и промолчал на изменении соседнего. Это то, чего мы хотим.

Выбрали весь объект раздела. Пересчитывается на любое изменение внутри раздела — потому что объект каждый раз новый. Выигрыш, ради которого заводили хранилище, потерян.

Выбрали с фильтром прямо в селекторе. Тоже пересчитывается всегда, и по более коварной причине: фильтр создаёт новый массив при каждом вызове. Даже если данные не менялись, новый массив не равен прошлому.

Селектор с запоминанием. Ноль пересчётов в обоих случаях: операции не менялись, значит и результат тот же самый объект.

Важно

Правило: выбирайте самое узкое. Не раздел, а поле. Не объект, а нужное значение из него. Сравнение идёт по ссылке, и чем крупнее выбранное, тем чаще оно оказывается «новым».

Отвечено вопросов: 0 из 2
Компонент выбирает из хранилища весь объект раздела. Что не так?
Селектор делает фильтр прямо внутри. Почему это ловушка?
Отвечено верно: 0 из 2

Селектор с запоминанием​

import { createSelector } from "@reduxjs/toolkit";

export const selectDeposits = createSelector(
(s) => s.vault.ops,
(ops) => ops.filter((o) => o.kind === "deposit"),
);
ПОЧЕМУ ФИЛЬТР ВНУТРИ СЕЛЕКТОРА — ЭТО ЛОВУШКАs => s.vault.ops.filter(…)фильтр создаёт новый массив при каждом вызовепрошлый !== новый — всегдапересчёт на любое изменение хранилищаcreateSelector(вход, вычисление)вход не изменился — вернётся прошлый результатпрошлый === новыйноль пересчётов, пока ops те жесравнение идёт по ссылке: новый массив с тем же содержимым — это другое значението же правило, что делало «изменение на месте» невидимым: библиотека сравнивает ссылки, а не поля
Вход не изменился — возвращается прошлый результат, тот же самый объект
Нажми на часть выражения

Когда это нужно: всегда, когда селектор что-то вычисляет — фильтрует, складывает, сортирует, собирает объект. Если селектор просто достаёт поле, запоминание не нужно: поле и так то же самое.

Отправка действий из компонента​

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

Обратите внимание: два отдельных обращения к хранилищу вместо одного за объектом. Так компонент пересчитается на изменение суммы и на изменение признака занятости — но не на появление новой операции в списке.

Совет

Хранить в общем хранилище то, что набирается в поле, — спорное решение: значение нужно одной форме. Здесь так сделано намеренно, чтобы показать связку; в настоящем проекте текст поля чаще оставляют в состоянии компонента, а в хранилище кладут только результат.

Отвечено вопросов: 0 из 2
Что делает селектор с запоминанием?
Когда запоминание не нужно?
Отвечено верно: 0 из 2

Типы: два вывода из хранилища​

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 }) — новый объект при каждом вызове. Либо два отдельных обращения, либо селектор с запоминанием.

Забыли обёртку хранилища в корне. Падение с сообщением о том, что обращение сделано вне провайдера.

Разделы по компонентам, а не по данным. Через неделю никто не понимает, где что лежит.

Состояние описано типом вручную. Со временем расходится с настоящим содержимым хранилища.

В хранилище кладут то, что набирается в поле. Работать будет, но каждое нажатие клавиши станет действием в общей истории.

Отвечено вопросов: 0 из 2
Зачем выводить тип состояния из хранилища, а не писать руками?
По какому признаку делят состояние на срезы?
Отвечено верно: 0 из 2

Слова главы​

  • Срез — описание раздела: имя, начальное состояние, обработчики; из него получаются действия и обработчик.
  • Создатель действия — функция, возвращающая объект действия; сам он ничего не делает.
  • Сборка хранилища — карта «имя раздела → его обработчик».
  • Обёртка хранилища — ставится в корне; без неё обращения падают.
  • Селектор — функция от состояния к значению; выбирать надо самое узкое.
  • Сравнение по ссылке — причина, по которой новый массив с тем же содержимым считается изменением.
  • Селектор с запоминанием — возвращает прошлый результат, пока входы не изменились.
  • Тип состояния из хранилища — выводится, а не пишется руками.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

export const vault = createSlice({ name: "vault", initialState, reducers: {} });

На чемпионате​

Разделов два-три, по данным. Кошелёк, операции. Больше не нужно.

Селекторы — рядом со срезом, а не в компонентах. Тогда при изменении формы состояния правится одно место.

Узкие селекторы с самого начала. Переписывать потом дороже, чем написать сразу.

Четыре строки типов — в первый же час. Иначе в каждом селекторе придётся писать тип состояния руками, и однажды он разойдётся.

И практическое: на защите откройте средства отладки и покажите список действий. Читаемые имена вроде «vault/opAdded» рассказывают историю сеанса лучше, чем любые слова.

Куда углубиться​

  • Обработчики чужих действий — как один раздел реагирует на действие другого.
  • Сущности в виде карты по идентификатору — готовое средство для списков.
  • Селекторы с параметром — и почему у них своя ловушка с запоминанием.
  • Сравнение по содержимому — когда сравнения по ссылке не хватает.
  • Проверка на случайное изменение состояния — встроенная и включена по умолчанию в разработке.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоУрок 4.1. Redux Toolkit. Часть 1Web-garage
Видео1. Redux Toolkit. Сделай redux код проще!Ulbi TV
ВидеоRedux Toolkit для управления состоянием в React-приложенииМихаил Непомнящий
ВидеоRedux-toolkit простым языкомДмитрий Герасимов - о разработке в сфере IT просто
ВидеоУчимся работать с Redux Toolkit (RTK) на примере управления продуктами и корзинойСалимов Руслан IT

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.