用一个纯粹的 reducer 来组织复杂状态,集中管理状态转换并排除不可能出现的组合。
在 Kodokon 中打开本课一旦某个组件需要协调多个一起演变的状态值——请求状态、一些数据、一个错误——不断增加 useState 调用就会把状态转换逻辑分散到每一个事件处理函数里。你很清楚这个症状:要按正确的顺序调用三个 setter,迟早会出现不一致的状态,比如状态是 success 却又并排放着一个非空的错误。useReducer 把职责反了过来:状态变成单一对象,而每一次变更都通过一个由中央函数(reducer)解释的 action 来完成。
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);
}
}reducer 是一个纯函数:它接收当前状态和一个 action,然后返回下一个状态,没有任何副作用。好处立竿见影:转换是显式且穷尽的,不可能的状态从源头上消失(每个 case 都重建一个一致的对象),而且逻辑集中在一处,而不是散落各处。一个有用的警示信号:如果你发现自己在两个地方复制粘贴同一串 setState 调用,那你很可能缺少一个 reducer。
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。