57. Zustand Middleware с TypeScript
TypeScript: Zustand Middleware с TypeScript
Заголовок раздела «TypeScript: Zustand Middleware с TypeScript»Привет, коллеги-волшебники кода! Сегодня мы погрузимся в одну из самых мощных, но иногда недооцененных фишек Zustand – Middleware. Если вы уже освоили базовые принципы управления состоянием с Zustand, то пришло время поднять свой скилл на новый уровень, научившись перехватывать, модифицировать и расширять логику работы вашего стора.
Представьте, что ваш Zustand стор – это оживленная фабрика. Обычные экшены – это заказы, которые поступают на склад, обрабатываются и отправляются на конвейер, где из них создается конечный продукт – новое состояние. Middleware – это специальные станции контроля качества или модификации на этом конвейере. Они могут логировать каждый “заказ”, изменять его содержимое перед обработкой, откладывать его выполнение или даже полностью блокировать, если он не соответствует требованиям. Звучит мощно, правда? Давайте разберемся, как это работает на практике.
Что такое Middleware в Zustand?
Заголовок раздела «Что такое Middleware в Zustand?»Middleware в Zustand – это функции высшего порядка, которые оборачивают вашу логику обновления состояния. Они позволяют вам выполнять код до или после того, как состояние будет изменено, а также получить доступ к текущему состоянию, функции set (для изменения состояния) и get (для получения текущего состояния).
Zustand использует create функцию, которая принимает StateCreator. Middleware же позволяет вам обернуть этот StateCreator.
Базовая сигнатура middleware выглядит примерно так (мы ее скоро типизируем):
(set, get, api) => next => state => stateГде:
set,get,api: Эти параметры предоставляются “внешним” контекстом Zustand.next: Это следующая функция в цепочке middleware или, в конечном итоге, самsetиз вашегоStateCreator. Вы должны ее вызвать, чтобы продолжить выполнение.state: Это функция, которая принимает частичное состояние для обновления.
На первый взгляд, это может выглядеть пугающе, но на самом деле это просто композиция функций.
Создаем свое Middleware: Логирование
Заголовок раздела «Создаем свое Middleware: Логирование»Давайте начнем с простого, но очень полезного примера: middleware для логирования всех изменений состояния. Это бесценно для отладки.
import { create, StateCreator, StoreApi } from 'zustand';
// 1. Определение типа состояния нашего стораinterface BearState { bears: number; increasePopulation: () => void; decreasePopulation: () => void;}
// 2. Создаем наш логгер-middleware// Правильно типизируем middleware для нашего состоянияtype LoggerMiddleware<T extends object> = ( config: StateCreator<T>) => StateCreator<T>;
const logMiddleware: LoggerMiddleware<BearState> = (config) => (set, get, api) => { return config( (args) => { console.log(' 🔥 Происходит действие:', args); // Логируем до изменения состояния set(args); // Вызываем оригинальный set для изменения состояния console.log(' ✅ Новое состояние:', get()); // Логируем после изменения состояния }, get, api );};
// 3. Создаем стор, применяя middlewareconst useBearStore = create<BearState>()( logMiddleware( (set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), decreasePopulation: () => set((state) => ({ bears: state.bears - 1 })), }) ));
// Пример использования (вы можете запустить это в браузере или Node.js)// useBearStore.getState().increasePopulation(); // Выведет логи в консоль// useBearStore.getState().increasePopulation();// useBearStore.getState().decreasePopulation();В этом примере logMiddleware оборачивает исходный StateCreator. Когда вызывается set внутри экшена, наш логгер перехватывает его, выводит информацию, затем вызывает оригинальный set, и только потом выводит новое состояние.
Продвинутые Сценарии: Цепочки Middleware и Дополнительная Логика
Заголовок раздела «Продвинутые Сценарии: Цепочки Middleware и Дополнительная Логика»Middleware раскрывает свой потенциал, когда мы начинаем их комбинировать и встраивать более сложную логику.
Представим, что нам нужно:
- Логировать все действия (как выше).
- Добавить метку времени к определенным действиям.
- Использовать встроенный
devtoolsдля отладки в браузере.
import { create, StateCreator, StoreApi } from 'zustand';import { devtools, persist } from 'zustand/middleware'; // Импортируем devtools
interface TaskState { tasks: { id: string; text: string; completed: boolean; createdAt?: number }[]; addTask: (text: string) => void; toggleTask: (id: string) => void;}
// 1. Наш логгер-middleware (с улучшенной типизацией)type ZustandMiddleware<T extends object> = (config: StateCreator<T, [], []>) => StateCreator<T, [], []>;
const logger: ZustandMiddleware<TaskState> = (config) => (set, get, api) => { return config( (args) => { console.groupCollapsed('🚀 Действие!'); console.log(' ➡️ Предыдущее состояние:', get()); console.log(' ➡️ Вызов:', args); set(args); console.log(' ✅ Новое состояние:', get()); console.groupEnd(); }, get, api );};
// 2. Middleware для добавления метки времени при создании задачиconst addTimestamp: ZustandMiddleware<TaskState> = (config) => (set, get, api) => { return config( (partial, replace) => { // Здесь мы проверяем, является ли частичное обновление объектом // и содержит ли оно функцию addTask или является ли это результатом addTask if (typeof partial === 'function') { // Если partial это функция, вызываем ее, чтобы получить новое состояние const newState = partial(get()); // Проверяем, если newState содержит tasks и мы добавляем новую задачу if (Array.isArray(newState.tasks) && newState.tasks.length > get().tasks.length) { const newTask = newState.tasks[newState.tasks.length - 1]; if (!newTask.createdAt) { newTask.createdAt = Date.now(); // Добавляем метку времени } } return set(newState, replace); // Применяем измененное состояние }
// Если partial это объект, пытаемся модифицировать напрямую if ('tasks' in partial && Array.isArray(partial.tasks) && partial.tasks.length > get().tasks.length) { const newTask = partial.tasks[partial.tasks.length - 1]; if (typeof newTask === 'object' && newTask !== null && !newTask.createdAt) { (newTask as any).createdAt = Date.now(); } } return set(partial, replace); }, get, api );};
// 3. Создаем стор, применяя несколько middlewareconst useTaskStore = create<TaskState>()( devtools( // Встроенный middleware для DevTools logger( // Наш кастомный логгер addTimestamp( // Наш кастомный middleware для метки времени (set) => ({ tasks: [], addTask: (text: string) => set((state) => ({ tasks: [...state.tasks, { id: Math.random().toString(36).substring(7), text, completed: false }], })), toggleTask: (id: string) => set((state) => ({ tasks: state.tasks.map((task) => task.id === id ? { ...task, completed: !task.completed } : task ), })), }) ) ), { name: 'Task Store' } // Имя для DevTools ));
// Пример использования// useTaskStore.getState().addTask('Изучить Zustand Middleware');// useTaskStore.getState().addTask('Написать домашку по TS');// useTaskStore.getState().toggleTask(useTaskStore.getState().tasks[0].id);
// Обратите внимание на порядок: devtools оборачивает logger, который оборачивает addTimestamp.// Выполнение идет от самого внешнего к внутреннему, а затем обратно.// set -> devtools -> logger -> addTimestamp -> ваш StateCreator -> изменение состоянияВ этом примере addTimestamp демонстрирует более сложную логику, где мы анализируем изменения состояния, чтобы инжектировать дополнительные данные. Обратите внимание, что partial в функции set может быть как объектом, так и функцией, возвращающей объект, поэтому важно обрабатывать оба случая.
Типичные Ошибки и их Решения
Заголовок раздела «Типичные Ошибки и их Решения»-
Забыли вызвать
setилиnext: Если ваше middleware не вызываетset(в контекстеconfigвнутри middleware) илиnext(если вы пишете middleware в стиле(set, get, api) => next => state => state), то состояние просто не будет обновляться.- Решение: Всегда убеждайтесь, что цепочка вызовов
setилиnextпродолжается.
- Решение: Всегда убеждайтесь, что цепочка вызовов
-
Неправильная типизация: Особенно актуально для продвинутых случаев. Если вы не указываете правильные дженерики или типы для
StateCreator, TypeScript может “потерять” информацию о вашем состоянии внутри middleware.- Решение: Используйте
StateCreator<T, [], []>илиStateCreator<T, Mwa, Mwb>для типизацииconfigвнутри вашего middleware, гдеT- это тип вашего состояния, аMwaиMwb- типы middleware, которые вы ожидаете снаружи и внутри соответственно (для простых случаев[]достаточно). А также используйтеtypeof partial === 'function'для безопасного доступа к частичным обновлениям.
- Решение: Используйте
-
Неверный порядок Middleware: Порядок имеет значение! Middleware, находящиеся “ближе” к
create, выполняются первыми. Еслиpersistстоит послеdevtools, тоdevtoolsувидит состояние до того, как оно будет загружено изlocalStorage.- Решение: Обдумывайте, какое middleware должно перехватывать первым. Например,
devtoolsобычно идет самым внешним, чтобы видеть все изменения, проходящие через остальные middleware.
- Решение: Обдумывайте, какое middleware должно перехватывать первым. Например,
🎯 Практика
Заголовок раздела «🎯 Практика»Ваш черед, Яша! Создайте следующие middleware для нашего TaskState (из примера выше) и скомбинируйте их.
undoRedoMiddleware: Реализуйте middleware, которое отслеживает историю измененийtasksи позволяет выполнять “отмену” (undo) и “повтор” (redo) последнего действия. Добавьте вTaskStateновые экшены:undoиredo, а также свойстваpastTasks,futureTasks.debounceMiddleware: Создайте middleware, которое откладывает выполнение определенного экшена (например,addTask) на заданное время, если он вызывается слишком часто. Только последнее из серии быстрых вызовов должно быть выполнено.validationMiddleware: Middleware, которое проверяет, что текст задачи не пустой и не содержит запрещенных слов (например, “badword”). Если валидация не проходит, экшенaddTaskне должен изменять состояние, а middleware должно вывести сообщение в консоль.- Комбинация: Примените все три ваших middleware вместе с
devtools. Убедитесь, чтоdevtoolsвидит финальное состояние после всех преобразований и что порядок middleware логичен.
💡 Совет
Заголовок раздела «💡 Совет»Zustand middleware – это мощный инструмент, но используйте его с умом. Как правило, middleware должны быть ответственны за одну конкретную вещь (логирование, персистирование, аутентификация и т.д.). Избегайте создания “монстр-middleware”, которые делают слишком много. Если логика становится слишком сложной, возможно, стоит пересмотреть архитектуру вашего стора или разбить сложное middleware на несколько более простых, сфокусированных функций. И всегда уделяйте внимание типизации – это ваш лучший друг в мире TypeScript!