Структурируй сложное состояние с помощью чистого редьюсера, который централизует переходы и исключает невозможные комбинации.
Открыть этот урок в KodokonКак только компонент управляет несколькими значениями состояния, которые меняются вместе - статусом запроса, данными, ошибкой - множество вызовов useState разбрасывает логику переходов по всем обработчикам событий. Симптом тебе знаком: три сеттера, которые надо вызвать в правильном порядке, и рано или поздно несогласованное состояние, например статус success рядом с ненулевой ошибкой. useReducer переворачивает ответственность: состояние становится единым объектом, а каждое изменение проходит через действие, которое интерпретирует одна центральная функция - редьюсер.
function requestReducer(state, action) {
switch (action.type) {
case "start":
return { status: "loading", data: null, error: null };
case "resolve":
return {
status: "success",
data: action.data,
error: null,
};
case "reject":
return {
status: "error",
data: null,
error: action.error,
};
default:
throw new Error("Unknown action: " + action.type);
}
}Редьюсер - чистая функция: он получает текущее состояние и действие, а затем возвращает следующее состояние, без побочных эффектов. Выгода видна сразу: переходы становятся явными и исчерпывающими, невозможные состояния исчезают в корне (каждый case заново собирает согласованный объект), а логика читается в одном месте, а не разбросана по коду. Полезный тревожный сигнал: если ты копируешь одну и ту же последовательность вызовов setState в двух местах, тебе, скорее всего, не хватает редьюсера.
function UserProfile() {
const [state, dispatch] = useReducer(requestReducer, {
status: "idle",
data: null,
error: null,
});
async function load(id) {
dispatch({ type: "start" });
try {
const res = await fetch("/api/users/" + id);
const data = await res.json();
dispatch({ type: "resolve", data });
} catch (error) {
dispatch({ type: "reject", error });
}
}
return <button onClick={() => load(1)}>Load</button>;
}Выбор между useState и useReducer - не вопрос вкуса. Оставь useState для простых независимых значений; переходи на useReducer, когда следующее состояние зависит от предыдущего или когда несколько полей меняются вместе. Два дополнительных плюса: третий аргумент даёт ленивую инициализацию (useReducer(reducer, arg, init) запускает init только при первом рендере), а React гарантирует, что dispatch сохраняет одну и ту же идентичность от рендера к рендеру - его можно передавать детям без useCallback.