Что такое useReducer в React?
Что такое useReducer
useReducer — встроенный хук React, который позволяет управлять состоянием компонента через концепцию редюсера. Это альтернатива useState для случаев, когда логика обновления состояния сложна или следующее состояние зависит от предыдущего.
Сигнатура
const [state, dispatch] = useReducer(reducer, initialState);
- reducer — чистая функция
(state, action) => newState - initialState — начальное значение состояния
- state — текущее состояние
- dispatch — функция для отправки действий
Когда использовать useReducer вместо useState
- Состояние — это объект с несколькими взаимосвязанными полями
- Следующее состояние зависит от предыдущего
- Логика обновления состояния сложная и её нужно вынести отдельно
- Несколько обработчиков событий выполняют похожие изменения состояния
Структура редюсера
Редюсер — чистая функция: не мутирует состояние, не вызывает побочных эффектов, при одинаковых входных данных всегда возвращает одинаковый результат.
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
function counterReducer(state: number, action: Action): number {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
case 'reset':
return action.payload;
default:
return state;
}
}
Ленивая инициализация
useReducer принимает необязательный третий аргумент — функцию init, которая вычисляет начальное состояние лениво. Полезно, если вычисление начального значения дорогостоящее:
function init(initialCount: number) {
return { count: initialCount };
}
const [state, dispatch] = useReducer(reducer, initialCount, init);
Сравнение с useState
| Критерий | useState | useReducer |
|---|---|---|
| Простое скалярное значение | Предпочтителен | Избыточен |
| Объект с несколькими полями | Возможен | Предпочтителен |
| Сложная логика переходов | Труден в поддержке | Хорошо структурирует |
| Тестируемость логики | Тестируется внутри компонента | Редюсер тестируется отдельно |
Связка с useContext
Один из популярных паттернов — передавать dispatch через контекст, чтобы любой дочерний компонент мог отправлять действия без prop drilling:
const DispatchContext = createContext<React.Dispatch<Action> | null>(null);
function Parent() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<Child />
</DispatchContext.Provider>
);
}
Этот паттерн является облегчённой заменой Redux для локального состояния приложения.
Что хочет услышать интервьюер
Кандидат понимает разницу между useState и useReducer и умеет объяснить, в каких случаях предпочтительнее каждый из них
Знание структуры редюсера: чистая функция (state, action) => newState, типизация action через дискриминированные union-типы
Понимание того, что редюсер должен быть чистой функцией — без мутаций и побочных эффектов
Осведомлённость о ленивой инициализации через третий аргумент
Знание паттерна связки useReducer + useContext как альтернативы Redux
Пример: Управление формой с useReducer
import { useReducer } from 'react';
interface FormState {
username: string;
email: string;
isSubmitting: boolean;
error: string | null;
}
type FormAction =
| { type: 'setField'; field: keyof Pick<FormState, 'username' | 'email'>; value: string }
| { type: 'submit' }
| { type: 'submitSuccess' }
| { type: 'submitError'; message: string };
const initialState: FormState = {
username: '',
email: '',
isSubmitting: false,
error: null,
};
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'setField':
// Обновляем только одно поле, остальные сохраняем
return { ...state, [action.field]: action.value, error: null };
case 'submit':
return { ...state, isSubmitting: true, error: null };
case 'submitSuccess':
return { ...initialState };
case 'submitError':
return { ...state, isSubmitting: false, error: action.message };
default:
return state;
}
}
export function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'submit' });
try {
await fetch('/api/register', {
method: 'POST',
body: JSON.stringify({ username: state.username, email: state.email }),
});
dispatch({ type: 'submitSuccess' });
} catch {
dispatch({ type: 'submitError', message: 'Ошибка регистрации' });
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={state.username}
onChange={e => dispatch({ type: 'setField', field: 'username', value: e.target.value })}
placeholder="Имя пользователя"
/>
<input
value={state.email}
onChange={e => dispatch({ type: 'setField', field: 'email', value: e.target.value })}
placeholder="Email"
/>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Отправка...' : 'Зарегистрироваться'}
</button>
</form>
);
}
Типичные ошибки
Мутация состояния внутри редюсера вместо возврата нового объекта
Использование useReducer для простых случаев, где достаточно useState — излишнее усложнение
Отсутствие case default в switch, что может привести к возврату undefined при неизвестном action.type
Размещение побочных эффектов (fetch, таймеры) внутри редюсера вместо useEffect
Неправильная типизация action: использование any вместо дискриминированных union-типов TypeScript


