Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке useReducer в React?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useReducer** - це хук React для керування станом через чисту функцію-редюсер: `(state, action) => newState`. ```jsx function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } } const [state, dispatch] = useReducer(reducer, { count: 0 }); dispatch({ type: 'increment' }); // стан стає { count: 1 } ``` **Ключове:** обирай його коли є 3+ типи оновлень або коли поля стану залежать одне від одного. Для простого boolean достатньо `useState`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useReducer** - це хук React, який керує станом через чисту функцію-редюсер: приймає поточний стан і дію, повертає наступний стан. ## Теорія ### TL;DR - Аналогія: торговий автомат. Ти dispatch-иш дію (монета + кнопка), редюсер обчислює новий стан (видає снек), сам автомат не змінюється в процесі. - Головна різниця від `useState`: вся логіка переходів стану живе в одній функції поза компонентом, а не розкидана по обробниках подій. - Використовуй `useReducer` коли є 3+ типи оновлень або коли зміна одного поля зачіпає інше. - Для одного boolean чи простого лічильника - `useState` без зайвого шаблонного коду. - Редюсер - це звичайна функція. Для її тестування React-імпорти не потрібні. ### Швидкий приклад ```jsx import { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; // обов'язково обробляй невідомі дії } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> <p>Count: {state.count}</p> {/* починається з 0 */} <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </> ); } ``` `dispatch({ type: 'increment' })` надсилає дію в редюсер. Редюсер повертає `{ count: 1 }`. React бачить новий стан і перерендерює компонент. Ось весь цикл. ### Ключова різниця від useState `useState` підходить для одного-двох незалежних значень. Коли стан - це об'єкт з кількома полями і зміна одного залежить від іншого, обробники подій починають нести забагато логіки. `useReducer` переносить цю логіку в одну функцію, яку можна читати, тестувати й розуміти окремо від компонента. Компонент залишається чистим - він тільки викликає `dispatch`, а не містить саму логіку оновлення. ### Коли використовувати - 1-2 незалежні значення без умов між ними - `useState`. - Об'єкт стану з 3+ різними типами оновлень (додати, видалити, скинути, фільтрувати) - `useReducer`. - Одне поле залежить від іншого (тогл тільки якщо не loading, інкремент тільки до певної межі) - `useReducer`. - Хочеш тестувати переходи стану без рендеру компонента - тестуй редюсер як звичайну функцію напряму. - Міграція з Redux - `useReducer` слідує тому ж патерну. ### useState проти useReducer | Характеристика | useState | useReducer | |---|---|---| | Найкраще для | Прості значення, незалежні поля | Складні об'єкти, кілька дій | | Як оновлюєш | `setCount(n)` | `dispatch({ type: 'inc' })` | | Де живе логіка | В обробниках подій | В одній функції-редюсері | | Тестування | Через компонент | Редюсер як звичайна функція | | Батчинг React 18 | Добре для більшості випадків | Краще для глибоких оновлень | | Коли переходити | 1-2 сетери | 3+ дії, взаємозалежний стан | ### Як React обробляє dispatch React ставить `dispatch`-виклики в чергу і батчить їх у concurrent mode (React 18+). При наступному fiber commit React синхронно запускає редюсер з останнім станом і дією, обчислює наступний стан і планує перерендер тільки якщо результат відрізняється. Сама функція `dispatch` стабільна між рендерами - її не потрібно додавати в масиви залежностей. Одна річ, яку я бачив як регулярну проблему в командах: редюсер виконується синхронно, тому fetch або API-виклики всередині нього неможливі. Редюсер повинен бути чистим. ### Типові помилки **1. Мутація стану в редюсері** ```jsx // неправильно function badReducer(state, action) { state.count++; // мутує спільний стан напряму return state; // повертає те саме посилання } ``` React використовує поверхневе порівняння. Повернення того самого посилання на об'єкт - немає перерендеру. Завжди повертай новий об'єкт: ```jsx // правильно case 'increment': return { ...state, count: state.count + 1 }; ``` **2. dispatch всередині тіла render** ```jsx // неправильно - нескінченний цикл function Counter() { const [state, dispatch] = useReducer(reducer, initialState); dispatch({ type: 'tick' }); // виконується кожен рендер, тригерить наступний } ``` Переноси `dispatch` в обробники подій або `useEffect`. **3. Забутий default case** ```jsx // неправильно function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; // немає default - невідомі дії повернуть undefined } } ``` Завжди додавай `default: return state`. Без нього будь-яка нерозпізнана дія повертає `undefined` і ламає компонент так, що важко відстежити причину. **4. Передача функції-оновлювача як дії** ```jsx // неправильно - звичка від useState dispatch(count => count + 1); // useReducer передає цю функцію як сам об'єкт дії // правильно dispatch({ type: 'increment' }); // обчислюй нове значення в редюсері ``` `useReducer` не приймає функції-оновлювачі. Другий аргумент редюсера - це завжди та дія, яку ти dispatch-нув. **5. useReducer для одного boolean** ```jsx // зайве const [state, dispatch] = useReducer(reducer, { isOpen: false }); dispatch({ type: 'toggle' }); // простіше const [isOpen, setIsOpen] = useState(false); setIsOpen(prev => !prev); ``` Більше шаблонного коду, ніж задача яку вирішуємо. Переходь на `useReducer` коли він справді потрібен. ### Де зустрічається - **TodoMVC з документації React**: один редюсер обробляє add, toggle і filter дії для списку задач. - **Async fetch**: dispatch `loading`, `success`, `error` по ходу запиту - чистіше ніж три окремі `useState`. - **Next.js App Router**: `useReducer` + `useEffect` для клієнтського стану після серверних мутацій. - **Zustand**: всередині використовує схожий підхід з функціями-редюсерами для слайсів стану. - **Правило з документації React**: якщо компонент потребує більше 3 сетерів або ти пишеш `if/switch` логіку в обробниках - час переходити на `useReducer`. ### Питання на співбесіді **Q:** Коли вибрати `useReducer` замість `useState`? **A:** Коли оновлення стану взаємозалежні. Наприклад, форма де при submit потрібно одночасно поставити `loading: true` і очистити `error`. З `useState` це два окремі виклики, які можуть розсинхронізуватись. З `useReducer` один `dispatch({ type: 'submit' })` обробляє обидва атомарно. **Q:** Що означає "чистий редюсер" і чому це важливо? **A:** Чистий - читає тільки `state` і `action`, повертає новий об'єкт, без побічних ефектів. Саме чистота дозволяє React 18 безпечно батчити виклики і робить можливим відтворення переходів стану при відлагодженні. **Q:** Як обробляти async операції з `useReducer`? **A:** Редюсер залишається синхронним. Dispatch-иш `{ type: 'loading' }` перед fetch, потім `{ type: 'success', payload: data }` або `{ type: 'error', payload: message }` в `.then`/`.catch`. `useEffect` відповідає за async частину, `useReducer` - за переходи стану. **Q:** Яка різниця між `useReducer` і Redux? **A:** `useReducer` локальний для одного компонента - немає глобального store, провайдера, middleware. Redux додає глобальний store, devtools і middleware. Для логіки одного компонента `useReducer` достатньо; для стану що потрібен в багатьох місцях - комбінують з Context або беруть Redux/Zustand. **Q:** (Senior) Як реалізувати оптимістичне оновлення з відкатом через `useReducer`? **A:** Зберігаєш знімок поточного стану перед дією. Dispatch `{ type: 'optimistic_update', data: newData }` - показуєш зміну одразу. При помилці API dispatch `{ type: 'rollback', snapshot: previousState }` і редюсер відновлює знімок. Жодних додаткових бібліотек. ## Приклади ### Базовий: лічильник з трьома діями ```jsx import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: return state; } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <p>{state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'reset' })}>Скинути</button> </div> ); } ``` Три дії в одному редюсері. Додати четверту - наприклад `set` з конкретним значенням - зміна тільки в редюсері, компонент не чіпаємо. ### Середній: список задач з фільтрацією ```jsx import { useReducer } from 'react'; const initialState = { todos: [], filter: 'all' }; function todosReducer(state, action) { switch (action.type) { case 'add_todo': return { ...state, todos: [...state.todos, { id: Date.now(), text: action.text, done: false }] }; case 'toggle': return { ...state, todos: state.todos.map(todo => todo.id === action.id ? { ...todo, done: !todo.done } : todo ) }; case 'set_filter': return { ...state, filter: action.filter }; default: return state; } } function TodoApp() { const [state, dispatch] = useReducer(todosReducer, initialState); const visible = state.filter === 'all' ? state.todos : state.todos.filter(t => t.done === (state.filter === 'done')); return ( <div> <input onKeyDown={e => { if (e.key === 'Enter') { dispatch({ type: 'add_todo', text: e.target.value }); e.target.value = ''; } }} placeholder="Додати задачу..." /> <ul> {visible.map(todo => ( <li key={todo.id} onClick={() => dispatch({ type: 'toggle', id: todo.id })} style={{ textDecoration: todo.done ? 'line-through' : 'none' }} > {todo.text} </li> ))} </ul> <button onClick={() => dispatch({ type: 'set_filter', filter: 'all' })}>Всі</button> <button onClick={() => dispatch({ type: 'set_filter', filter: 'done' })}>Виконані</button> </div> ); } ``` Три пов'язаних поля - `todos`, `filter` і обчислений `visible` - оновлюються в одному місці. Спробуй написати те саме на `useState` і порахуй скільки місць прийдеться чіпати для кожної нової фічі. ### Складний: async fetch зі станами loading і error ```jsx import { useReducer, useEffect } from 'react'; const initialState = { data: null, loading: false, error: null }; function asyncReducer(state, action) { switch (action.type) { case 'loading': return { ...state, loading: true, error: null }; case 'success': return { data: action.payload, loading: false, error: null }; case 'error': return { ...state, loading: false, error: action.payload }; default: return state; } } function UserProfile({ userId }) { const [state, dispatch] = useReducer(asyncReducer, initialState); useEffect(() => { dispatch({ type: 'loading' }); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => dispatch({ type: 'success', payload: data })) .catch(err => dispatch({ type: 'error', payload: err.message })); }, [userId]); if (state.loading) return <p>Завантаження...</p>; if (state.error) return <p>Помилка: {state.error}</p>; return <pre>{JSON.stringify(state.data, null, 2)}</pre>; } ``` Редюсер ніколи не торкається `fetch`. `useEffect` ніколи не торкається стану напряму. Кожна частина має одне завдання - такий поділ добре масштабується коли компонент росте.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.