useReducer: reducer как чистая функция
Урок про useReducer: когда сложное состояние удобнее описывать чистой функцией-редьюсером, и почему такой редьюсер легко тестировать.
Reducer — чистая функция
(state, action) => newState: по текущему состоянию и описанию действия она вычисляет следующее состояние, ничего не мутируя и не обращаясь к внешнему миру.
Когда useState становится тесным
useState отлично подходит для независимых простых значений. Но когда у состояния много полей, между ними есть связи, а переходы нетривиальны (несколько setState подряд, зависящих друг от друга), код расползается. useReducer собирает всю логику переходов в одном месте — редьюсере — и компонент лишь «диспатчит» действия.
const [state, dispatch] = React.useReducer(reducer, initialState);
// в обработчике:
dispatch({ type: "increment", by: 2 });
Редьюсер — это чистая функция (запускаемый пример)
Главная ценность: редьюсер не зависит от React. Это обычная функция, которую можно вызвать и проверить в чистом JS. Вот редьюсер счётчика-корзины и прогон нескольких действий:
function reducer(state, action) {
switch (action.type) {
case "add":
return { ...state, count: state.count + 1, items: [...state.items, action.name] };
case "remove":
return {
...state,
count: Math.max(0, state.count - 1),
items: state.items.slice(1),
};
case "reset":
return { count: 0, items: [] };
default:
return state;
}
}
let state = { count: 0, items: [] };
const actions = [
{ type: "add", name: "Книга" },
{ type: "add", name: "Ручка" },
{ type: "remove" },
{ type: "add", name: "Тетрадь" },
];
for (const a of actions) {
state = reducer(state, a);
console.log(a.type, "=>", JSON.stringify(state));
}
console.log("reset =>", JSON.stringify(reducer(state, { type: "reset" })));
Вывод:
add => {"count":1,"items":["Книга"]}
add => {"count":2,"items":["Книга","Ручка"]}
remove => {"count":1,"items":["Ручка"]}
add => {"count":2,"items":["Ручка","Тетрадь"]}
reset => {"count":0,"items":[]}
Обратите внимание: каждое действие возвращает новый объект (через ...state и slice), а не мутирует старый. Это та же иммутабельность, что и в useState — React сравнивает ссылки.
Чем это лучше
- Логика в одном месте. Все переходы видны в редьюсере, а не размазаны по обработчикам.
- Тестируемость. Редьюсер — чистая функция: подал
stateиaction, проверил результат, без рендера и моков. - Предсказуемость. Состояние меняется только через явные действия с понятными именами.
- Стабильный
dispatch. React гарантирует, что ссылка наdispatchне меняется, — её безопасно передавать в memo-детей безuseCallback.
useReducer или useState?
| Берите useState | Берите useReducer |
| 1–2 независимых значения | несколько связанных полей |
| простые присваивания | сложные переходы, зависящие от прошлого состояния |
| логика умещается в обработчике | хочется собрать переходы и протестировать их |
Итог
useReducerописывает переходы состояния чистой функцией(state, action) => newState.- Редьюсер не зависит от React — его легко прогнать и протестировать в обычном JS.
- Возвращайте новый объект состояния, не мутируйте старый.
- Подходит для сложного связанного состояния; для пары простых значений хватит
useState.