Zustand - это стейт-менеджер, в котором под каждую новую сущность создаётся отдельный стор, что сплиттит код и облегчает начальный бандл
Старт проекта
Основы работы
Slice
Создание слайса выглядит следующим образом:
model / counterSlice.ts
Получение данных из state
Чтобы использовать данные из стейта, мы импортируем наш хук и вставляем из него данные в компонент
App.tsx
Использование actions
Основные пункты видео:
Введение в экшены:
Экшены - функции для изменения состояний в Store.
Всю логику работы с данными (получение из API, отправка запросов) рекомендуется выносить в Store.
Цель урока:
Научиться создавать экшены для увеличения и уменьшения счетчика.
Типизация экшенов:
Создание типа CounterActions для типизации экшенов increment и decrement.
Работа с экшенами в Store:
Использование функций set и get для взаимодействия с экшенами и стейтом.
Изменение счетчика:
Создание и типизация экшенов для увеличения (increment) и уменьшения (decrement) значения счетчика.
Примеры упрощенной записи для изменения значения счетчика.
Интеграция экшенов в UI:
Получение экшенов в компоненте через хук.
Создание кнопок и присвоение им обработчиков onClick для выполнения экшенов increment и decrement.
Результат:
Демонстрация работы кнопок в браузере - нажатие изменяет значение счетчика.
model / counterSlice.ts
И теперь тут мы просто получаем наши экшены вместе со значением в сторе
App.tsx
Использование вне компонента
Цель:
Получение и изменение Store вне компонентов React.
Шаги реализации:
Создание Вспомогательной Функции:
Создайте папку helpers c файлом addTen.ts в корне проекта.
В addTen.ts, определите функцию, изменяющую значение счётчика на 10 в зависимости от его текущего состояния (минус 10, если меньше нуля и плюс 10, если больше или равно нулю).
Ограничения Хуков:
Хуки React не могут использоваться вне компонентов, условных выражениях, и после условных return. Создание прямого доступа к состоянию и действиям Store обходит эти ограничения.
Экспорт Стейта и Экшенов из Store:
Вернитесь к CounterStore и создайте новый экшен changeByAmount для изменения счетчика на переданное значение.
Используйте метод getState из useCounterStore для получения доступа к созданному экшену и стейту. Для актуализации данных счетчика, используйте функцию-геттер.
Использование Вспомогательной Функции:
Уберите лишние вызовы хуков и импорты.
В функции addTem, используйте геттер для получения текущего значения счетчика и измените его значение с помощью changeByAmount в зависимости от условий.
Тестирование Результатов:
Добавление кнопки в UI для вызова функции addTem.
Проверка в браузере: счетчик должен уменьшаться на 10, если меньше нуля, и увеличиваться на 10, если равно нулю или больше.
Иногда перед нами может появиться такая ситуация, когда нам нужно использовать стор-значения вне компонента и менять его данные так же извне. Для этого мы можем сделать отдельный хелпер, который внутри себя будет выполнять операции над стором
`model / counterSlice.ts
Далее в отдельной функции получаем наше значение и выполняем любые логические операции, которые нам понадобятся
helpers / addTen.ts
И теперь можем в любой части приложения выполнить этот код, который мы вынесли в отдельный участок приложения
App.tsx
Отладка
В реальных проектах часто приходится работать с большим количеством сторов и экшенов. Для отладки кода размерным методом является использование консоли или дебаггера.
Решение:
Предложено использовать Redux DevTools для отладки, что значительно упрощает процесс.
Установка Redux DevTools совершается через добавление расширения в браузер.
Подключение Middleware:
Middleware требуется для работы с Redux DevTools.
Импорт devtools из Zustand Middleware и его подключение к приложению.
Настройка:
Добавление Middleware в список дженериков типа StateCreator через массив типов.
Исправление потенциальной ошибки путём коррирования функции создания стора (Create) с использованием Middleware.
Работа с Redux DevTools:
После подключения Middleware, при открытии Redux DevTools видно состояние сторов, выполненные экшены и таймлайн изменений.
Для лучшей отладки важно добавлять названия экшенов при их вызове для удобства отслеживания в DevTools.
Дополнительные настройки:
В экшенах можно указать параметр replace, определяющий, будет ли изменяемая часть стора полностью заменена на новую после выполнения экшена.
Подписывание каждого экшена специфическим образом позволяет лучше ориентироваться в выполненных действиях при просмотре через DevTools.
Отладка в Zustand происходит за счёт использования ReduxDevtools, который работают очень костыльно и требуют дополнительного бойлерплейта, чтобы подтянуться к ним
Сейчас у нас представлен код маленького todo-листа
`model / todoStore.ts
App.tsx
Асинхронные операции
Асинхронные операции + Запрос на сервер
Проблема Размещения Асинхронных Операций в Компонентах:
Размещение запросов к API и других асинхронных операций в компонентах размазывает логику обработки данных по всему приложению.
Это увеличивает сложность компонентов и делает код труднее для понимания.
Преимущества Использования Store:
Централизация асинхронных операций в Store упрощает управление состоянием и взаимодействие с API.
Обеспечивает возможность переиспользования логики запросов без дублирования кода.
Содействует поддержанию компонентов максимально простыми и фокусированными на представлении.
Преимущества Zustand как State Manager:
Несмотря на простоту и легкость, Zustand предоставляет мощные возможности для управления состоянием.
Помогает в оптимизации и предотвращении ненужных ререндеров.
Подготовка:
Очистка проекта: Удаление неактуальных элементов.
Создание структуры:
coffeeStore.ts в папке models для описания стора.
coffeeTypes.ts в папке types для типизации работы с API.
Элементы UI: контейнер и карточки напитков (с информацией о напитке, рейтингом, изображением, и кнопкой покупки).
Стилизация:
Определены стили для контейнера и карточек.
Настройка Store (Zustand):
Данные и Экшены:
State для списка напитков.
Экшен getCoffeeList для получения списка напитков.
Создание слайса:
Использование StateCreator для создания слайса.
Конфигурация middleware (DevTools).
Асинхронный запрос:
Асинхронная функция в экшене для загрузки данных из API.
Обработка ошибок и обновление состояния с полученными данными.
Работа с Компонентами:
Получение и отображение данных:
Удаление заглушек, использование хука для доступа к состоянию.
Запрос данных при первой загрузке с помощью useEffect.
types / coffeTypes.ts
model / coffeeStore.ts
App.tsx
Persist
Цель:
Обучить сохранению состояния в Zustand при помощи Middleware Persist чтобы избежать потери данных при перезагрузке страницы, например, для токенов аутентификации, пользовательского ввода, пагинации, фильтров и т.д.
Основные шаги:
Введение в Проблему:
При перезагрузке страницы данные, хранящиеся в Zustand, теряются. Это проблема, например, при необходимости сохранять пользовательскую сессию или введенные данные.
Решение с Middleware Persist:
Zustand предоставляет инструмент Middleware Persist для сохранения состояния при перезагрузке.
Применение на Практике:
На примере простого приложения с счетчиком показано, как подключить Middleware Persist. Сначала закомментирован код не относящийся к счетчику, затем добавлен каунтер в разметку.
Настройка Middleware:
В CounterStore типизирована работа с Middleware. Добавлен Middleware Persist, обернут вызов CounterSlice. Параметрами указано название стора.
Результат:
После изменений в браузере и перезагрузки страницы состояние каунтера сохраняется, благодаря автоматическому сохранению в LocalStorage.
Настройка Сохраняемых Данных:
Добавлен второй счетчик для демонстрации возможности сохранения только определенных данных, используя параметр Partialize в Persist, что позволяет указывать, какие данные сохранять.
Использование Разных Хранилищ:
Можно использовать различные хранилища, не ограничиваясь LocalStorage. Будет также показано создание кастомного хранилища.
model / counterStore.ts
App.tsx
Reset состояния
Основная идея: Обучающее видео о том, как создавать хранилища для управления состояниями в приложении и как реализовать сброс к изначальным состояниям.
Часть 1: Основы сброса состояний
Рассмотрена необходимость не только установки и хранения значений в Store, но и сбрасывания к изначальным состояниям.
Показан простой способ создания метода для сброса состояний, возвращающий значения переменных к исходным (например, в 0).
Пример добавления кнопки “Reset” для вызова метода сброса.
Часть 2: Сброс состояний в нескольких хранилищах
Введена проблематика сброса состояний в нескольких Store одновременно.
Объяснена необходимость кастомизации функции создания Store (Create функции) для решения вышеупомянутой проблемы.
Часть 3: Кастомизация функции создания Store
Создание файла для кастомной функцииCreate:
Функция собирает методы сброса из каждого Store и вызывает их всего одним действием.
Для хранения методов сброса используется Set, предотвращая повторения.
Реализация кастомной функцииCreate:
Написание функции, аналогичной оригинальной, но с дополнительной логикой для сброса нескольких состояний одновременно.
Внедрение возможности получения изначального состояния Store и создание общего метода сброса, добавляемого в Set.
Применение кастомной функции:
Переопределение импорта функции Create в Store на кастомную версию.
Демонстрация работы сброса состояний через новую кнопку “Reset”.
src/helpers/create.ts
src/model/counterStore.ts
App.tsx
Продвинутые техники
Подписки на store
Цели:
Улучшить читаемость и производительность кода
Осуществить сохранение пользовательского ввода в URL-параметрах
Шаги:
Введение в проблему сохранения состояния
Текущая реализация поиска теряет данные при перезагрузке страницы
Целесообразно сохранять такие данные в параметрах URL
Создание отдельного стора для поиска
Удаление ненужных сторов и хелперов (CounterStore, TodoStore)
Создание нового стора SearchStore с состоянием для поиска
Структура SearchStore
SearchState: хранит текст поиска как необязательный параметр типа string
SearchActions: включает действие setText для обновления текста поиска
Работа с подписками в Zustand
Введение в использование subscribe для отслеживания изменений стейта
Пример подписки на изменения в searchStore и связывание его с другими сторами
Стратегия работы с подписками
Использование подписок для реагирования на изменения состояний
Пример: автоматическое обновление списка напитков при изменении состояния поиска
Кастомные хранилища
Цель:
Разработать систему, позволяющую сохранять и восстанавливать состояние приложения через параметры URL.
Шаги реализации:
Создание hashStorage:
Создать хранилище (hashStorage) в директории helpers, используя пример из документации.
Объявить тип хранилища как StateStorage.
Настройка Middleware Persist:
Подключить Middleware Persist в SearchStore и CoffeeStore.
Для SearchStore, настроить использование hashStorage вместо стандартного localStorage.
Кастомизация хранилища:
Для использования других типов хранилищ, например, SessionStorage, написать функции getItem, setItem, и removeItem.
Показать, как данные сохраняются и восстанавливаются при перезагрузке страницы и при открытии в новой вкладке.
Сохранение состояния в URL:
Иллюстрация того, как параметры сохраняются в URL, позволяя перезагрузить страницу без потери информации.
Улучшение UX:
Внедрение механизма, позволяющего сохранить текст запроса при перезагрузке страницы для последующего использования при инициализации.
App.tsx
Улучшенный стор в URL
Проблема изначального решения:
Хранение стейта поиска в hash URL, плохо масштабируется при усложнении (например, добавлении пагинации, фильтров).
Сильная связанность компонентов и сторов, усложняющая поддержку и развитие.
Неудобство работы с несколькими сторами из-за ограничений persist функции.
Подход к улучшению:
Создание кастомного хука для работы с URL Storage (useURLStorage.ts):
Использовать ReactRouterDOM для взаимодействия с URL.
Использование типизации для удобства работы c параметрами.
Перенос логики из компонентов для повторного использования и упрощения компонентной структуры.
Логика работы хука:
Извлекаем параметры из URL и синхронизируем с внутренним стором приложения.
Обновление URL параметров при изменении стейта приложения для отражения текущего состояния.
Упрощение строки запроса в URL для удобства пользователей.
Преимущества подхода:
Улучшенная масштабируемость и поддерживаемость кода.
Уменьшение связанности компонентов и сторов.
Упрощение работы с URL и облегчение навигации для пользователя.
Практическое применение:
Обернуть приложение в браузер роутер из ReactRouterDOM для корректной работы.
Использование созданного хука в главном компоненте app.tsx для обработки параметров поиска и пагинации.
Slice паттерн
I. Введение в компонентный подход
Проблема: Приложение разрослось, все в одном файле.
Решение: Переход к компонентному подходу - разбиение на несколько файлов.
II. Декомпозиция компонентов
Создание папки components.
Вынос карточки напитка
Экспорт необходимых компонентов и функций из AntDesign и Store.
III. Декомпозиция State Management
Проблема: CoffeeStore становится слишком большим.
Решение: Разделение на несколько файлов или слайсов для упрощения управления и поддержки.
IV. Создание отдельных слайсов
Удаление ненужных Store и создание новых слайсов, например, CartSlice и ListSlice.
Использование StateCreator для определения типов и состояний.
Декомпозиция типов и экшенов для точечного использования в слайсах.
V. Консолидация и оптимизация типов
Создание файла StoreTypes для удобного экспорта и импорта типов.
Внесение необходимых изменений в слайсы для корректной работы и типизации.
VI. Финальная интеграция и рефакторинг
Упрощение CoffeeStore путем удаления дубликатов и лишних типов.
Создание общего Store, объединяющего CartSlice и ListSlice.
Описание только необходимой логики в файле store, что облегчает поддержку приложения.
VII. Заключение: Преимущества подхода
Улучшение читаемости кода и облегчение его поддержки.
Более эффективная работа в команде за счет четкого разделения логики и компонентов.
Повышение продуктивности разработки за счет декомпозиции и оптимизации структуры приложения.
Предотвращение рендеров
Цель:
Уменьшить количество ненужных ререндеров в приложении для улучшения производительности.
Инструменты:
React DevTools для визуализации ререндеров.
Хук useShallow для оптимизации доступа к данным.
Шаги оптимизации:
Визуализация Ререндеров:
Установите расширение React DevTools.
Во вкладке разработчика, активируйте опцию “Highlight Updates when component re-render” для визуальной отметки ререндеров.
Проблема:
Вся страница перерендеривается при любом изменении данных, так как стор связан с каждым компонентом.
Решение Проблемы ЧерезuseShallow:
Избавьтесь от глобального вызова стора (useCoffeeStore) в верхнем уровне приложения.
Разделите компоненты, использующие данные из стора, на отдельные модули.
Для каждого компонента, вызывайте стор только там, где это необходимо.
Примеры Имплементации:
Поиск: В searchInput.tsx, получите доступ к методам и параметрам стора, используя useShallow.
Список Кофе: Аналогично, изолируйте компоненты, работающие со списком кофе и данными корзины.
Методы и Параметры: Перенесите логику useEffect и другие взаимодействия со стором в соответствующие компоненты.
Результаты:
Изменение параметров поиска изменяет только список кофе, не вызывая ререндер всей страницы.
Добавление кофе в корзину не приводит к ререндеру других компонентов страницы, за исключением самой корзины.
Работа с TanStack Query
TanStack Query - это мощная библиотека для контроля запросов на клиентской части приложения. Она принимает, инвалидирует и кэширует запросы.
Добавляем клиент ReactQuery и ReactQueryDevtools для диагностики отправляемых запросов в тулзе
main.tsx
Далее нам нужно реализовать хук, который обернёт в себя запрос из Zustand с помощью useQuery и вернёт нужные нам поля (сами данные + состояния запроса по типу isLoading, isError, isSuccess)
helpers / useCustomQuery.ts
Только для того, чтобы RQ смог вернуть и закэшировать наш запрос, нужно, чтобы функция возвращала данные, а не сохраняла их просто в стор. Тут мы переделали функцию getCoffeeList, чтобы она возвращала data
model / listSlice.ts
И поменяли тип возврата на Promise от типа кофе
model/storeTypes.ts
Далее нам нужно подключить наш хук useCustomQuery в инпуте поиска, который будет подтягивать данные через RQ
components / SearchInput.tsx
src/helpers/useUrlStorage.tsx
Immer middleware
Что мы хотим сделать? Нам нужно выводить один и тот же напиток не друг за другом, а считать их количество и выводить его сбоку, чтобы не раздувать слишком сильно список.
В этом деле, чтобы не мутировать объекты в нашем сторе, мы можем воспользоваться прослойкой в виде immer, который позволяет не мутировать объекты, но задавать им новые значения просто указывая целевое поле для изменения.
Устанавливаем immer
Дополняем наш persist небольшой конфигурацией immer
model/coffeeStore.ts
И теперь нам нужно будет заменить код в функции set, вставив туда produce из immer. Теперь мы можем себе позволить не деструктуризировать проект, а сразу мутировать то, что нам прилетает.
В общем работа выглядит следующим образом: мы получаем в produce определённый draft нашего состояния, который мы изменяем. Потом immer подставит этот draft в наш стейт. Вторым аргументом мы получаем немутированные данные в state для изменения нашего черновика, который пойдёт в стор.
Так же в StateCreator нужно передать дополнительное обозначение типа ["zustand/immer", unknown]