🏠Домашнее задание
⚡ 1) Зачистить стейт при вылогинивании
Если вылогиниться из приложения, то таски и тудулисты, которые находятся в стейте не зачищаются. И это не есть хорошо. В данном видео описана проблема и ее реализация на redux. Ваша задача сделать это на redux toolkit
⚡2) Работа с селекторами
Рефакторим все селекторы в приложении согласно auth.selectors.ts

В данном видео (запись с урока) максимально подробно рассказываю про работу с селекторами
-----------------------------------------------------------------------------------------------------
⚡1) Intro
Давайте зайдем в компонент App.tsx
и посмотрим как мы достаем данные из стейта
const isLoading = useAppSelector((state) => state.app.isLoading);
Казалось бы и что тут рассказывать, но на самом деле есть нюансы которые нужно знать
⚡2) useAppSelector
⚡2.1) Вводная часть
Давайте перепишем доставание isLoading
вот так
const { isLoading } = useAppSelector((state) => state.app);
Я довольно часто вижу такую запись. Почему так студенты любят писать. Допустим нам из app стейта нужно достать 3 значения (isLoading
, isAppInitialized
, unHandleActions
)
И теперь сравним 2 варианта
const { isLoading, isAppInitialized, unHandleActions } = useAppSelector((state) => state.app);
Сравнивая 1 и 2 вариант, очевидно что в 1 случае кода писать меньше, именно поэтому так и любят писать.
Теперь проверьте, поменялось ли что-нибудь или нет ?
Ответ: Нет, ничего не поменялось. Значит получается без разницы как писать? А вот здесь ответ нет, разница существенная.
⚡ 2.2) Начнем со 2 варианта Вот такой у нас будет стартовый код
export const App = () => {
console.log("App render");
const dispatch = useAppDispatch();
const isLoading = useAppSelector((state) => state.app.isLoading);
const isAppInitialized = useAppSelector((state) => state.app.isAppInitialized);
const unHandleActions = useAppSelector((state) => state.app.unHandleActions);
// const { isLoading, isAppInitialized, unHandleActions } = useAppSelector((state) => state.app);
console.log("isLoading: ", isLoading);
console.log("isAppInitialized: ", isAppInitialized);
console.log("unHandleActions: ", unHandleActions);
useEffect(() => {
setTimeout(() => {
dispatch(appActions.setIsLoading({ isLoading: true }));
}, 3000);
}, []);
return <div className="App">{isLoading && <LinearProgress />}</div>;
};
Вот, что мы увидим в консоли

isLoading
изменил свое состояние, соответственно компонент App перерисовался. Все логично, все хорошо.
теперь давайте в useEffect через 3 секунды будем менять error (вмеcто
isLoading
)
dispatch(appActions.setAppError({ error: "Error" }));

⚡ 2.3) Теперь все тоже самое проделаем для 1 варианта
1) Для первого варианта, когда меняем крутилку ничего не поменялось
2) Но когда мы меняем ошибку, то получим следующий итог

В App.tsx
мы не достаем error, но она все равно перерисовывается 🤯🤯🤯
Берем максимально точечно данные из стейта
Никогда не используем деструктуризация в селекторах (Можно этим правилом пренебречь в том случае, если уверены, что все свойства стейта используются в компоненте)
⚡3) Selector
Опять возвращаемcя к нашему исходному селектору
const isLoading = useAppSelector((state) => state.app.isLoading);
⚡3.1) Как правило в разных компонентах нам нужно доставать одинаковые данные из стейта. Т.е. Код я указал выше может использовался в 10 компонентах.
А теперь представьте что у нас в приложении мы решели заменить свойство isLoading
на loadingStatus.
К чему это приведет?
Это приведет к тому, что нам нужно идти в 10 компонент и менять эти свойства, что не очень приятно.
⚡3.2) При каждом рендере - новая функция. Cелектор будет вызываться при каждом рендере, а не только когда обновились данные в сторе.
⚡3.3) Логика получения данных из структуры стора находится внутри компонента. Но зачем компоненту знать об этом?
По этим причинам селекторы выносят в отдельные функции, которые в принципе так и называют.
Создадим файл app.selector.ts
и вынесем туда кода доставания значения из стейта

Функции селекторы называют по разному. Встречал следующие варианты:
selectIsLoading
isLoadingSelector
иногда вообще в названии не указывают select (selector) и группируют все селекторы в один объект (namespace). Я так не делал никогда : )
Все селекторы выносим в файл с селекторами
Селекторы кладем в отдельные файлы соответствующие фиче.
⚡4) Reselect
Говоря про селекторы нельзя не упомянуть про createSelector
⚡4.1) Теория
Селекторы можно разделить на 2 типа
простые (обычные) селекторы - это селекторы которые просто достают данные из стейта
const selectCount = (state: RootState) => state.counter.value;
const isLoadingSelector = (state: RootState) => state.app.isLoading;
const packsSelector = (state: RootState) => state.packs.cardPacks;
Такие селекторы стоит использовать всегда, когда мы напрямую ссылаемся на данные из стора. Даже если возвращаемое значение является объектом, не стоит беспокоиться - мы лишь возвращаем ссылку на уже существующий объект, который находится в стейте, так что ни к каким проблемам это не приведет.
сложные селекторы, это селекторы в которых мы делаем сортировку / фильтрацию или сложные вычисления
const selectSubtotal = createSelector(selectShopItems, items =>
items.reduce((subtotal, item) => subtotal + item.value, 0)
)
const selectTax = createSelector(
selectSubtotal,
selectTaxPercent,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
const selectTotal = createSelector(
selectSubtotal,
selectTax,
(subtotal, tax) => ({ total: subtotal + tax })
)
⚡4.2) Практика
Задача Отображать только те колоды у которых в имени содержится буква f.
в проекте с карточками поиск, фильтрации, сортировка реализована на сервере, но ради примера давайте будем с сервера запрашивать 100 колод и фильтровать их по имени локально.
1) Захаркодим запрос за 100 колодами
getPacks: () => {
return instance.get<FetchPacksResponseType>("cards/pack?pageCount=100");
}
2) Вынесем получение колод в селектор
import { RootState } from "app/store";
export const packsSelector = (state: RootState) => state.packs.cardPacks;
3) Теперь сделаем сложный селектор в котором собственно и будем осуществлять логику по фильтрации
export const filteredByNamePacksSelector = (state: RootState) => {
console.log("filteredByNamePacksSelector");
const packs = state.packs.cardPacks;
return packs.filter((p) => p.name.includes("f"));
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
Теперь воспользуйтесь данным селектором в Packs.tsx. Попробуйте фильтровать по разным буквам и убедитесь, что все отрабатывает верно 👍
4) Для того, чтобы понять что собственно не так, давайте компонент Counter
(мог бы быть абсолютно любой компонент в котором используется useSelector) уберем из роутинга и вставим на одном уровне с App
root.render(
<Provider store={store}>
<App />
<Counter />
<RouterProvider router={router} />
<GlobalError />
</Provider>
);
5) Напишем console.log('render ${component}')
в Packs.tsx и Counter.tsx
И теперь измените меняйте значение Counter

Мы меняем значение счетчика, который вообще никоим боком не связан с колодами, но мы видим как она перерисовывается 🤯🤯🤯.
6) И вот чтобы избежать вот такого поведения нам и нужен createSelector
export const filteredByNamePacksSelector = createSelector(
// 1 - массив селекторов
[packsSelector],
// 2 - функция, которая принимает данные от селекторов и возвращает новое значение
(packs) => {
console.log("filteredByNamePacksSelector");
return packs.filter((p) => p.name.includes("f"));
}
);
По итогу получим вот такой результат 🚀

🔗 Примеры работы с useSelector из документации
☝ Вывод по использованию createSelect такой
1) Используйте мемоизированные селекторы когда:
В селекторе есть тяжелые вычисления (фильтрация, сортировка, сложное преобразование данных, и так далее)
Результатом вызова селектора является объект. Ну и конечно же, это касается массивов и различных структур вроде Set и Map, так как они тоже являются объектами.
2) Есть мнение о том чтобы без анализа селектора всегда использовать reselect
Last updated
Was this helpful?