Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Redux?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Redux** - це передбачуваний контейнер стану для JavaScript-додатків. Він централізує всі оновлення стану через чисті reducer-функції, що реагують на dispatched action-об'єкти. ```javascript const reducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; default: return state; } }; const store = createStore(reducer); store.dispatch({ type: 'ADD_TODO', payload: 'Купити молоко' }); // store.getState() → ['Купити молоко'] ``` **Ключова ідея:** один глобальний store, оновлення тільки через dispatched actions, стан ніколи не мутується напряму.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Redux** - це передбачуваний контейнер стану для JavaScript-додатків, який централізує всі зміни через чисті reducer-функції, що реагують на прості action-об'єкти. ## Теорія ### TL;DR - Redux схожий на єдину бухгалтерську книгу компанії: кожна зміна (action) записується бухгалтерами (reducers), які створюють нову сторінку (state), не стираючи попередню - Один глобальний store замість розкиданого локального стану в компонентах - Три основні частини: store (тримає стан), actions (описують події), reducers (чисті функції оновлення) - Використовуй, якщо логіка стану повторюється в 5+ компонентах або потрібне time-travel debugging - Redux Toolkit - сучасний підхід до написання Redux, прибирає приблизно 70% шаблонного коду ### Швидкий приклад ```javascript import { createStore } from 'redux'; const reducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; // Незмінне оновлення default: return state; } }; const store = createStore(reducer); store.dispatch({ type: 'ADD_TODO', payload: 'Купити молоко' }); console.log(store.getState()); // ['Купити молоко'] store.dispatch({ type: 'ADD_TODO', payload: 'Вигуляти собаку' }); console.log(store.getState()); // ['Купити молоко', 'Вигуляти собаку'] ``` Action описує *що сталось*. Reducer вирішує *що це означає для стану*. Store тримає результат. ### Головна різниця Локальний стан компонентів розкиданий по дереву. Кожен компонент, якому потрібні спільні дані, отримує їх через props або Context, і це швидко стає хаосом. Redux кладе все в одне дерево об'єктів, а єдиний спосіб щось змінити - відправити action. Це одне обмеження і робить великі додатки зручними для debug. ### Коли використовувати Redux - **Малий додаток, менше 5 компонентів зі спільним станом** - не варто. React Context або useState достатньо. - **Середній додаток з повторюваною логікою стану** - Redux Toolkit зі slices. - **Великий додаток з devtools або time-travel debugging** - повний Redux з middleware. - **Server-side або не-React JS додаток** - Redux core без React-прив'язок. - **Стан менше 10KB і простий** - розглянь Zustand, він легший. ### Як Redux працює всередині Коли викликаєш `store.dispatch(action)`, Redux пропускає action через middleware (thunks, логери), потім передає кореневому reducer. Кореневий reducer через `combineReducers` викликає кожен дочірній reducer. Кожен повертає новий фрагмент стану. Redux збирає фрагменти в новий об'єкт стану і повідомляє підписників. React's `useSelector` порівнює новий стан з попереднім за посиланням і перерендерює тільки ті компоненти, чий вибраний фрагмент реально змінився. Чисті функції роблять це швидким. Незмінні оновлення дозволяють React.memo і `reselect` пропускати непотрібні рендери через просту перевірку посилання. ### Типові помилки **Мутація стану в reducer** ```javascript // Неправильно - мутує спільний масив function badReducer(state = [], action) { if (action.type === 'ADD') state.push(action.payload); // Пряма мутація! return state; } // Правильно - повертає новий масив function goodReducer(state = [], action) { if (action.type === 'ADD') return [...state, action.payload]; return state; } ``` Redux ділить посилання на стан між рендерами. Якщо мутуєш, всі підписники бачать зміну одразу, минаючи цикл reducer - і вся модель ламається. **Dispatch всередині render** ```javascript // Неправильно - виконується при кожному рендері function BadComponent() { dispatch(fetchData()); // Безкінечний цикл! return <div>...</div>; } // Правильно - виконується один раз при монтуванні function GoodComponent() { useEffect(() => { dispatch(fetchData()); }, []); return <div>...</div>; } ``` Це одна з найпоширеніших Redux-помилок у продакшені. Я бачив, як вона викликала сотні дублікатів API-запитів за одну сесію, перш ніж хтось це помітив. **Ігнорування Redux Toolkit** Писати vanilla Redux з `createStore` і `combineReducers` вручну в 2024 - це багато шаблонного коду без жодної користі. Toolkit дає Immer (пишеш мутативний синтаксис, отримуєш незмінні оновлення), auto-generated action creators і `createAsyncThunk` для async-потоків. Офіційна команда Redux рекомендує Toolkit для всіх нових проектів. **Race condition у thunks** ```javascript store.dispatch(fetchUser(1)); // відправлено першим, відповідає пізніше store.dispatch(fetchUser(2)); // відправлено другим, відповідає першим // Без захисту повільніший запит перезаписує результат швидшого ``` Рішення: зберігай `requestId` в стані коли запит стартує, потім у `fulfilled` перевіряй: якщо не збігається - пропускай оновлення. **Надмірна нормалізація малого стану** Вкладати `{ entities: { users: { 1: { name: 'Bob' } } } }` для одного користувача - зайвий боілерплейт без жодної вигоди. Тримай плоску структуру, поки не маєш більше 10 пов'язаних елементів. ### Де зустрічається в реальних проектах - **React / Next.js** - глобальний стан для auth, кошика, сповіщень (Shopify Hydrogen використовує RTK Query) - **Electron-додатки** - offline-first синхронізація стану (Discord desktop - відомий приклад) - **Node.js з WebSockets** - спільний стан сесій між з'єднаннями - **Vanilla JS** - стан гри для детермінованих реплеїв (поширено в Phaser-іграх) - **Альтернативи** - Zustand для додатків до 500 рядків, Jotai для атомарного стану, TanStack Query коли більшість стану - серверні дані ### Follow-up питання **Q:** Опиши Redux-потік від `dispatch` до re-render у React. **A:** Action потрапляє в dispatch, проходить через middleware (наприклад, thunk вирішує async-роботу), досягає кореневого reducer, який викликає дочірні через `combineReducers`. Кожен повертає новий фрагмент. Redux збирає новий об'єкт стану, повідомляє підписників. React's `useSelector` запускає селектор, порівнює результат за посиланням і перерендерює компонент, якщо він змінився. **Q:** Чому reducers мають бути чистими функціями? **A:** Чиста функція повертає однаковий результат для однакового вводу і не має побічних ефектів. Саме це робить time-travel debugging можливим: відтвори будь-яку послідовність actions - отримаєш той самий стан. Це також дозволяє порівняння за посиланням для продуктивності і робить unit-тести тривіальними. **Q:** Що Redux Toolkit додає порівняно з vanilla Redux? **A:** Immer (пишеш мутативний синтаксис, отримуєш незмінні оновлення), `createSlice` (автоматично генерує action creators і типи), `configureStore` (додає Redux DevTools і thunk middleware за замовчуванням), `createAsyncThunk` (обробляє стани pending/fulfilled/rejected) і RTK Query (fetching з кешуванням). Прибирає приблизно 70% шаблонного коду. **Q:** Як реалізувати `combineReducers` самостійно? **A:** ```javascript function combineReducers(reducers) { return (state = {}, action) => { return Object.keys(reducers).reduce((nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {}); }; } ``` Кожен ключ отримує свій фрагмент стану. Кореневий reducer викликає кожен дочірній і збирає результат. **Q:** Як боротися з race conditions у async thunks? **A:** `createAsyncThunk` з Redux Toolkit прикріплює `requestId` до кожного dispatched thunk. Зберігай останній `requestId` в стані на початку запиту. У `fulfilled` перевіряй: `if (action.meta.requestId !== state.currentRequestId) return;`. Це відкидає відповіді від застарілих запитів. ## Приклади ### Базовий Redux-потік ```javascript import { createStore } from 'redux'; const reducer = (state = { count: 0 }, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; const store = createStore(reducer); store.subscribe(() => console.log(store.getState())); store.dispatch({ type: 'INCREMENT' }); // { count: 1 } store.dispatch({ type: 'INCREMENT' }); // { count: 2 } store.dispatch({ type: 'DECREMENT' }); // { count: 1 } ``` Store тримає один об'єкт. Кожен dispatch проходить через reducer. Стан ніколи не змінюється на місці. ### Список задач з Redux Toolkit ```javascript import { configureStore, createSlice } from '@reduxjs/toolkit'; import { useSelector, useDispatch } from 'react-redux'; const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { addTodo: (state, action) => { state.push(action.payload); }, // Immer обробляє незмінність toggleTodo: (state, action) => { state[action.payload].completed = true; } } }); const store = configureStore({ reducer: { todos: todosSlice.reducer } }); function TodoList() { const todos = useSelector(state => state.todos); const dispatch = useDispatch(); return ( <ul> {todos.map((todo, i) => ( <li key={i} onClick={() => dispatch(todosSlice.actions.toggleTodo(i))}> {todo.text} </li> ))} <button onClick={() => dispatch(todosSlice.actions.addTodo({ text: 'Нове завдання', completed: false }))}> Додати </button> </ul> ); } ``` `createSlice` автоматично генерує action creators. Immer перехоплює `state.push(...)` всередині reducer і застосовує зміну як незмінне оновлення. ### Async thunk з обробкою race condition ```javascript import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; const fetchUser = createAsyncThunk('user/fetch', async (id) => { const response = await fetch(`/api/user/${id}`); return response.json(); }); const userSlice = createSlice({ name: 'user', initialState: { data: null, loading: false, currentRequestId: null }, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state, action) => { state.loading = true; state.currentRequestId = action.meta.requestId; // Відстежуємо поточний запит }) .addCase(fetchUser.fulfilled, (state, action) => { if (action.meta.requestId !== state.currentRequestId) return; // Відкидаємо застарілі відповіді state.data = action.payload; state.loading = false; }); } }); ``` Без перевірки `requestId` повільний перший запит може перезаписати результат швидшого другого. Цей патерн запобігає цьому.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.