useReducer виносить логіку оновлення стану в окрему функцію-редуктор: компонент лише описує, що сталося (дію), а редуктор вирішує, як змінюється стан.
function cartReducer(state, action) {
switch (action.type) {
case 'added':
return { ...state, items: [...state.items, action.item] };
case 'removed':
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case 'cleared':
return { ...state, items: [] };
default:
throw new Error(`Невідома дія: ${action.type}`);
}
}
function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
return <button onClick={() => dispatch({ type: 'cleared' })}>Очистити</button>;
}
Коли useReducer кращий:
- кілька пов'язаних значень змінюються разом (статус запиту, дані, помилка; кроки майстра);
- багато різних оновлень одного стану розкидано по обробниках - редуктор збирає їх в одному місці;
- наступний стан залежить від попереднього складним чином;
- тестування: редуктор - чиста функція, її тестують без React:
expect(cartReducer(state, action)).toEqual(...); - передача вниз:
dispatchстабільний між рендерами - його безпечно передавати в дочірні компоненти й контекст безuseCallback.
Коли досить useState: незалежні прості значення (відкрито/закрито, текст у полі).
Правила редуктора:
- чистий: без запитів, таймерів, випадкових значень - React може викликати його двічі в StrictMode;
- не мутує стан - повертає новий об'єкт;
- дії описують подію (
'added','login_failed'), а не «встановити поле» - тоді логіка зосереджена в редукторі, а не в компонентах.
Лінива ініціалізація - третій аргумент: useReducer(reducer, userId, createInitialState) - функція викличеться лише один раз.
Масштабування: useReducer + Context - простий спосіб керувати станом частини застосунку без зовнішніх бібліотек. Для складнішого - Redux Toolkit, ідеї якого побудовані на тих самих редукторах.