🏠Домашнее задание

⚡ 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 варианта Вот такой у нас будет стартовый код

App.tsx
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, но она все равно перерисовывается 🤯🤯🤯

Так происходит именно по причине неправильного доставания данных из стейта. Мы подписываемся в useSelector на весь app useAppSelector(state => state.app) Соответственно если в app стейте поменяется любое свойство, то все компоненты в которых данные достаются подобным образом будут перерисовываться вне зависимости используется там это свойство или нет

⚡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) Практика

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) Теперь сделаем сложный селектор в котором собственно и будем осуществлять логику по фильтрации

packs.selector.ts
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

Мы меняем значение счетчика, который вообще никоим боком не связан с колодами, но мы видим как она перерисовывается 🤯🤯🤯.

Такое поведение мы получаем из-за того, что наш сложный селектор возвращает новый массив.

useSelector не просто достает данные из стейта, он еще делает подписку. И рендер в компонентах происходит когда приходит новое значение.

Чтобы сравнить какому компоненту перерисовываться, под капотом у useSelector происходит сравнение изменились данные или нет.

Сравнение под капотом происходит по ссылке, а как мы знаем объекты / массивы сравниваются по ссылке и два одинаковых массива никогда не будут равны.

Максимально подробно эту тему освятил АйтиСиняк

6) И вот чтобы избежать вот такого поведения нам и нужен createSelector

createSelector - это функция из библиотеки reselect, которая позволяет создавать селекторы в Redux-приложениях. Селекторы используются для извлечения данных из хранилища Redux, а createSelector помогает оптимизировать производительность приложения, кэшируя результаты вызова селекторов и предотвращая повторные вычисления.

packs.selector.ts
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?