Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Redux middleware». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Redux middleware** перехоплює дії між `dispatch` і редюсером, дозволяючи перевіряти, змінювати, затримувати або скасовувати їх. ```javascript const loggerMiddleware = store => next => action => { console.log('dispatching:', action); const result = next(action); console.log('new state:', store.getState()); return result; }; const store = createStore(reducer, applyMiddleware(loggerMiddleware)); ``` **Головне:** виклик `next(action)` передає дію далі; якщо пропустити його, дія зупиняється.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Redux middleware** є функцією, яка знаходиться між `dispatch` і редюсером та перехоплює дії до того, як вони досягають сховища. ## Теорія ### TL;DR - Middleware схожий на сортувальний центр пошти: дії надходять, проходять обробку або перенаправлення, потім ідуть до редюсера - Сигнатура: `store => next => action => result` (три вкладені функції) - Виклик `next(action)` передає дію далі; пропустити цей виклик означає заблокувати дію повністю - Middleware підходить для побічних ефектів (async-запити, логування, аналітика); редюсер - для змін стану - Порядок важливий: `applyMiddleware(thunkMiddleware, loggerMiddleware)` запускає thunk першим ### Швидкий приклад ```javascript // Сигнатура: store => next => action => result const loggerMiddleware = store => next => action => { console.log('dispatching:', action); const result = next(action); // передаємо далі console.log('new state:', store.getState()); return result; }; const store = createStore(rootReducer, applyMiddleware(loggerMiddleware)); // Output: "dispatching: {type: 'ADD_TODO', payload: 'Learn Redux'}" // Output: "new state: {todos: [{id: 1, text: 'Learn Redux'}]}" ``` `next(action)` передає управління наступному middleware в ланцюжку. Після повернення можна зчитати оновлений стан. Це весь патерн. ### Як працює ланцюжок middleware Коли викликається `dispatch(action)`, Redux не відправляє дію одразу до редюсера. Він передає її через кожен middleware зліва направо. Кожен middleware є функцією вищого порядку: перший виклик отримує `store`, другий отримує `next` (посилання на наступний middleware або фінальний dispatch, якщо він останній), третій отримує саму `action`. Якщо middleware викликає `next(action)`, дія рухається далі. Якщо ні - зупиняється там і ніколи не досягає редюсера. Redux будує цей ланцюжок один раз під час `createStore()`, загортаючи оригінальний dispatch через `applyMiddleware()`. Каррінг тут не випадковий. `applyMiddleware` частково застосовує `store` до кожного middleware під час ініціалізації, потім об'єднує результати в ланцюг. Логіка обробки дій виконується тільки в момент dispatch, не під час створення сховища. ### Коли використовувати middleware Побічні ефекти належать в middleware, не в редюсери. Редюсери мають бути чистими функціями. - Async-операції (API-запити, таймери): Redux Thunk або Redux Saga - Логування і налагодження: фіксація кожної дії і зміни стану в dev-збірках - Аналітика: перехоплення конкретних типів дій і відправка подій до сервісу трекінгу - Обробка помилок: трансформація помилок до того, як вони потраплять до редюсера - Збагачення дій: додавання мітки часу або ID користувача до кожної вихідної дії - Скасування: блокування дій на основі поточного стану або умов ### Типові помилки **Подвійний виклик `next()`** ```javascript // НЕПРАВИЛЬНО - дія потрапляє до редюсера двічі const badMiddleware = store => next => action => { next(action); next(action); }; // ПРАВИЛЬНО - викликаємо next() рівно один раз const goodMiddleware = store => next => action => { console.log('before:', action); const result = next(action); console.log('after:', store.getState()); return result; }; ``` **Відсутній return** ```javascript // НЕПРАВИЛЬНО - ламає ланцюжок для підписників const badMiddleware = store => next => action => { next(action); // немає return }; // ПРАВИЛЬНО const goodMiddleware = store => next => action => { return next(action); }; ``` **Мутація об'єкту дії** ```javascript // НЕПРАВИЛЬНО - змінює оригінальну дію const badMiddleware = store => next => action => { action.timestamp = Date.now(); return next(action); }; // ПРАВИЛЬНО - створюємо новий об'єкт через spread const goodMiddleware = store => next => action => { return next({ ...action, timestamp: Date.now(), userId: store.getState().auth.userId }); }; ``` **Неправильний порядок middleware** ```javascript // НЕПРАВИЛЬНО - logger бачить функції до того, як thunk їх обробив const store = createStore( reducer, applyMiddleware(loggerMiddleware, thunkMiddleware) ); // ПРАВИЛЬНО - thunk першим перетворює функції на plain objects const store = createStore( reducer, applyMiddleware(thunkMiddleware, loggerMiddleware) ); ``` **Async-дії без Thunk** ```javascript // НЕПРАВИЛЬНО - dispatch очікує plain object dispatch(async () => { const data = await fetch('/api/data'); // ця функція ніколи не виконається }); // ПРАВИЛЬНО - додаємо thunk middleware const store = createStore(reducer, applyMiddleware(thunk)); dispatch((dispatch) => { fetch('/api/data').then(data => dispatch({ type: 'SET_DATA', payload: data })); }); ``` ### Де зустрічається в реальних проектах - **Redux Thunk** (найпоширеніший): dispatch-функції з async API-запитами - **Redux Saga**: складні потоки зі скасуванням і race conditions, використовується у великих проектах на кшталт фронтенду Uber - **Redux Observable**: на базі RxJS, для реактивних потоків - **Redux Logger**: логує кожну дію і зміну стану в dev-збірках - **Redux Persist**: перехоплює всі дії і автоматично синхронізує стан з localStorage - **Кастомний middleware для аналітики**: перехоплює конкретні типи дій і відправляє їх до сервісу трекінгу ### Питання на співбесіді **Q:** Чому middleware є каррінг-функцією (`store => next => action => result`), а не звичайною? **A:** Каррінг дозволяє Redux частково застосувати аргументи під час налаштування. `applyMiddleware` один раз викликає зовнішню функцію зі `store`, потім об'єднує результати в ланцюг. Логіка обробки дій виконується тільки в момент dispatch. **Q:** Що станеться, якщо middleware не викличе `next()`? **A:** Дія припиняє поширення і ніколи не досягає редюсера або наступних middleware. Підписники все одно отримують сповіщення про dispatch-виклик, що може призводити до неочікуваної поведінки, якщо стан фактично не змінився. **Q:** Чи може middleware читати поточний стан? **A:** Так. `store.getState()` доступний всередині middleware. Виклик до `next(action)` повертає стан до обробки дії, виклик після - вже оновлений стан. **Q:** В чому різниця між Redux Thunk і Redux Saga? **A:** Thunk простіший: дозволяє dispatch-ити функції замість plain objects. Saga використовує генератори і підтримує складні сценарії на кшталт скасування і race conditions. Thunk підходить для звичайних async-запитів; Saga потрібна, коли треба координувати кілька запитів або скасовувати ті, що вже в процесі. **Q:** (Рівень senior) Як запобігти race conditions, якщо одна async-дія відправляється кілька разів підряд? **A:** Відстежувати запити в процесі виконання за типом дії всередині middleware. Якщо такий тип вже є - ігнорувати або ставити в чергу новий dispatch. Redux Saga вирішує це через `takeLatest()`, який автоматично скасовує попередній запит при надходженні нового. ## Приклади ### Базовий: Logger middleware ```javascript const loggerMiddleware = store => next => action => { console.log('dispatching:', action.type); const result = next(action); console.log('new state:', store.getState()); return result; }; const store = createStore(rootReducer, applyMiddleware(loggerMiddleware)); store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' }); // dispatching: ADD_TODO // new state: { todos: [{ id: 1, text: 'Learn Redux' }] } ``` Простий у написанні, корисний під час розробки. Додавай після thunk в `applyMiddleware`, щоб бачити вже оброблені дії. ### Середній: Async middleware (патерн Thunk) ```javascript const thunkMiddleware = store => next => action => { // якщо дія є функцією, викликаємо її з dispatch і getState if (typeof action === 'function') { return action(store.dispatch, store.getState); } return next(action); }; const fetchUser = (userId) => async (dispatch, getState) => { dispatch({ type: 'FETCH_USER_START' }); try { const response = await fetch(`/api/users/${userId}`); const user = await response.json(); dispatch({ type: 'FETCH_USER_SUCCESS', payload: user }); } catch (error) { dispatch({ type: 'FETCH_USER_ERROR', payload: error.message }); } }; // В компоненті: store.dispatch(fetchUser(123)); // одразу dispatch-ить FETCH_USER_START, // потім FETCH_USER_SUCCESS або FETCH_USER_ERROR коли запит завершиться ``` Thunk перевіряє чи дія є функцією. Якщо так - викликає її з `dispatch` і `getState`. Якщо ні - передає далі звичайним чином. Це вся реалізація. ### Просунутий: Middleware для дедублікації ```javascript const dedupeMiddleware = store => { let lastAction = null; let lastTime = 0; return next => action => { const now = Date.now(); const isDuplicate = lastAction?.type === action.type && lastAction?.payload === action.payload && now - lastTime < 500; // одна й та сама дія за 500ms if (isDuplicate) { console.warn('Duplicate action blocked:', action.type); return; // пропускаємо next() повністю } lastAction = action; lastTime = now; return next(action); }; }; // dispatch({ type: 'SUBMIT_FORM', payload: formData }) двічі за 500ms // Другий dispatch блокується. До редюсера доходить тільки одна дія. ``` Зверни увагу на замикання (closure) над `lastAction` і `lastTime`. Middleware зберігає цей стан між dispatch-викликами, бо зовнішня функція виконується один раз під час ініціалізації сховища. Цей патерн я особисто застосовував у продакшені для захисту від подвійного сабміту форм на повільних з'єднаннях: кнопка спрацьовує двічі, але до сервера йде тільки один запит.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.