Reducer 与 Context 组合
Reducer 与 Context 组合 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
reducer 管逻辑,Context 管传递,两者组合就是一个小型全局状态方案。读完你应该能搭出一个课程编辑工作区:状态和 dispatch 分成两个 context 提供,多个子组件共享同一份状态,一次 dispatch 三处联动。
问题场景
课程编辑工作区有三块:工具栏(改课程名、加课时)、课时列表(选中、删除)、预览面板(只读展示当前课程)。它们操作的是同一份数据。用属性传递的话,state 和一串回调都得从顶层一路传下去;只给一个「大 context」也不理想——读取状态的组件被迫拿到 dispatch,职责混在一起。
核心解释
官方推荐的组合方式是两个 context 分工:一个装状态,一个装操作。
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function courseReducer(state, action) {
switch (action.type) {
case "RENAME": return { ...state, title: action.title };
case "ADD_LESSON": return { ...state, lessons: [...state.lessons, action.lesson] };
case "REMOVE_LESSON":return { ...state, lessons: state.lessons.filter(l => l.id !== action.id) };
case "SELECT_LESSON":return { ...state, selectedId: action.id };
default: return state;
}
}
export default function App() {
const [state, dispatch] = useReducer(courseReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
<Toolbar /> {/* 只用 dispatch */}
<LessonList /> {/* 读写都用 */}
<Preview /> {/* 只读 state */}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
这样拆的收益要在使用时才看得见:
- 组件接口干净。Toolbar 拿 DispatchContext 就能干活,不关心数据长什么样;Preview 只读不写,想让它出错都难。
- reducer 仍然可测。Context 只是分发通道,状态转移规则还在纯函数里,测法和 R18 完全一样。
- 重渲染范围明确。任何 dispatch 都会让
state换新对象,读 StateContext 的组件全部重渲染——这个工作区里三块都读,所以三块都会更新。如果某个分支很大很贵,把它的 Provider 范围缩小,或者拆分状态。
示例拆解
实验区的三块面板共享一个 reducer,先看只读面板的取数方式:
function Preview() {
const state = useContext(StateContext); // 只读:拿不到 dispatch
const current = state.lessons.find(l => l.id === state.selectedId);
return <p>课程「{state.title}」共 {state.lessons.length} 课时</p>;
}
- 工具栏:改名输入框(每敲一个字 dispatch
RENAME)和「添加课时」按钮; - 课时列表:每行带「选中」「删除」,dispatch
SELECT_LESSON/REMOVE_LESSON; - 预览面板:不拿 dispatch,只读 StateContext,展示课程名和课时数;
- 验证方式:在工具栏改名,预览同步变;在列表选中某一课,预览显示「当前第 N 课」;删除课时后列表和预览的计数一起变——三次操作都只有一次 dispatch,没有任何 setState。
动手任务
打开本课实验区,完成三个小改动:
- 新增
MOVE_UP动作:把选中的课时上移一位,第一位时忽略;在列表里加上移按钮。 - 给 Preview 加一个「已选课时标题」展示,只通过 StateContext 获取数据实现。
- 把 reducer 抽到独立文件并补 4 条转移测试(改名、添加、删除、非法动作返回原 state)。
常见错误
- 两个 context 用了同一个 value:把
{ state, dispatch }打包塞进一个 context,所有只读组件也要跟着 dispatch 变化重渲染;读写分开提供才能控制重渲染面。 - Provider 的 value 忘了用变量:
value={{ state, dispatch }}每次渲染都生成新对象,下游永远在「变化」;reducer 方案里 state 本身就是稳定引用,分开传正好规避。 - 把 effect 和请求塞进 reducer:Context 化之后这种冲动更强(「反正全局都有」);数据请求留在事件处理或专门的数据层,reducer 只做纯状态转移。
- 组件树外调用 useContext:context 只能在组件或 Hook 内读取;工具函数需要 dispatch 时,通过参数传进去。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能说清 StateContext 和 DispatchContext 各自的消费组件是谁
- 一次 dispatch 后能预测哪几个组件重渲染,并用实验验证
- 状态转移规则仍然集中在可独立测试的 reducer 里
参考资料
- React 官方文档:使用 Reducer 和 Context 扩展状态管理 —— 本课结构的官方出处,含读写分离写法
- React 官方文档:useReducer API —— reducer 签名与严格模式下的双重调用
读完这一课了?
标记完成后,工作台会安排接下来的学习。