useReducer 与状态转移
useReducer 与状态转移 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
练习提交这种多步骤流程,用四五个 useState 拼会越来越乱。读完你应该能用 useReducer 把整条状态转移收进一个纯函数,用动作名表达业务变化,并会测试非法操作被正确拒绝。
问题场景
做题页面有完整生命周期:编辑中 → 提交中 → 已批改,编辑中可以改标题和答案,提交中要锁住输入,已批改后只能重置。用 useState 写是这样:
const [step, setStep] = useState("idle");
const [title, setTitle] = useState("");
const [answer, setAnswer] = useState("");
const [log, setLog] = useState([]);
function handleSubmit() {
if (step !== "editing" || !answer.trim()) return; // 校验散落在事件里
setStep("submitting");
setLog(l => [...l, "提交中…"]);
}
每个操作都得先「看一眼现在是什么阶段,再决定改哪几个字段」。阶段越多,这些 if 就越分散,读代码的人拼不出完整的状态机。
核心解释
useReducer 的思路是反过来:把「现在是什么阶段」和「能做什么」收进一个纯函数。组件只负责发动作(dispatch),reducer 负责判断这个动作在当前状态下合不合法、状态该怎么变:
function reducer(state, action) {
switch (action.type) {
case "SUBMIT":
if (state.step !== "editing") return state; // 非法操作:原样返回
if (!state.answer.trim()) return { ...state, log: [...state.log, "提交被拒绝"] };
return { ...state, step: "submitting", log: [...state.log, "提交中…"] };
case "RESOLVE":
if (state.step !== "submitting") return state;
return { ...state, step: "done", log: [...state.log, "批改完成"] };
// ...
}
}
三个关键约定:
- 动作表达业务意图,不是赋值语句:叫
SUBMIT而不是SET_STEP_SUBMITTING,动作名就是需求文档。 - reducer 必须是纯函数:同样的 state 和 action 永远算出同样的结果,不发请求、不改外部变量。副作用留在组件的事件处理里。
- 非法操作直接忽略(返回原 state):调用方不用预判状态,reducer 是唯一闸门。整个状态机的合法路径一目了然:idle 只能进 editing,editing 才能提交,submitting 才能出结果。
示例拆解
实验区就是这套 reducer 驱动的提交流程:
- 界面按
state.step渲染:idle 显示「开始做题」,editing 显示标题、答案输入和提交按钮,submitting 锁输入,done 显示结果和重置; - 「模拟批改」按钮 dispatch
RESOLVE;你在 submitting 阶段外点它,界面纹丝不动——reducer 把它当成无效操作吞掉了; - 日志面板记录每次转移:进入编辑 / 提交中 / 批改完成 / 提交被拒绝,正好对应 reducer 里的每个 return;
- 验证方式:故意在 idle 点「提交」,日志出现「提交被拒绝」而不是状态错乱;清空答案提交同样被拒。
动手任务
打开本课实验区,完成三个小改动:
- 新增
CHANGE动作,只在 editing 阶段生效,让标题和答案可编辑。 - 新增
FAIL动作:submitting 阶段收到时回到 editing,日志记「批改失败」,其他阶段忽略。 - 给
SUBMIT加一条规则:答案少于 2 个字时拒绝并在日志里写明原因。
常见错误
- 在 reducer 里写副作用:发请求、setTimeout、改数组之外的变量都不行;reducer 可能在开发模式下被调用两次来帮你发现这类问题。
- 动作对象里塞函数或复杂逻辑:action 应该是「发生了什么」的描述(type + 载荷),计算放进 reducer。
- 直接改 state 返回:
state.log.push(...)后返回原对象,React 认为状态没变不重渲染;必须返回新对象。 - 用 reducer 但动作名还是赋值思维:
SET_TITLE、SET_STEP满天飞,等于把 useState 换了个地方写;先想清楚业务动作再建模。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能画出这个流程的状态机:idle / editing / submitting / done 和每条边的触发动作
- 知道非法操作的处理位置在 reducer 的守卫分支里,而不是每个调用点
- 写新功能时先列动作清单(业务语言),再动手写 reducer
参考资料
- React 官方文档:把状态逻辑提取到 Reducer 中 —— 从 useState 重构到 reducer 的完整过程
- React 官方文档:useReducer API —— 参数、返回值与类型细节
读完这一课了?
标记完成后,工作台会安排接下来的学习。