R20第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

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。

动手任务

打开本课实验区,完成三个小改动:

  1. 新增 MOVE_UP 动作:把选中的课时上移一位,第一位时忽略;在列表里加上移按钮。
  2. 给 Preview 加一个「已选课时标题」展示,只通过 StateContext 获取数据实现。
  3. 把 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 里

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1多选中等(多选,全部选对才算正确)

关于 Context 的使用边界,下列做法正确的有?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...