结合 Reducer 与 Context:构建清晰可扩展的全局数据层
在前面的两关中,你分别掌握了两个强大的工具:
- Reducer:让你把复杂的组件状态更新逻辑收拢到一个独立的纯函数中;
- Context:让你能够跨越层层嵌套,直接把数据送到深层组件手中。
但是,如果你只单独使用它们:
- 单独使用 Reducer 时,你依然需要把
tasks和dispatch作为 props 逐级传给深层的孙子组件; - 单独使用 Context 时,如果要修改深层数据,你不得不传递许多分散的 setter 函数。
如果把它们强强联合呢? Reducer 负责管理复杂业务逻辑,Context 负责将状态与分发能力全域广播! 这正是现代大型 React 应用最为核心、最为优雅的经典架构!
1. 架构设计:为什么要把 State 和 Dispatch 分开放入两个 Context?
我们推荐创建两个平行的 Context:
TasksContext:专门用来向下广播任务列表数据(供需要展示任务的组件读取);TasksDispatchContext:专门用来向下广播dispatch函数(供需要新增、删除任务的按钮读取)。
为什么不把它们打包成一个 value={{ tasks, dispatch }}?
这是一个极其精妙的性能与设计考量:
- 很多组件(比如页面底部的“清空已完成”按钮)只需要派发动作,根本不需要关心任务列表具体有几项;
- 如果把它们捆绑在一个 Context 中,每次任务列表一发生改变,那些原本只关心 dispatch 的组件也会被强迫一起重新渲染!
- 读写分离能让组件只订阅自己真正关心的那一部分,带来极其出色的性能表现和代码纯度。
2. 封装独立的 Provider 模块
为了让应用结构极其清晰,我们通常新建一个名为 TasksContext.js 的独立文件,把 Context、Reducer 和 Provider 全部内聚封装在里面:
// TasksContext.js
import { createContext, useContext, useReducer } from 'react';
// 1. 创建两个独立的 Context
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
// 2. 核心 Provider 组件
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
// 3. 导出两个超方便的自定义 Hook!
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
// 4. Reducer 逻辑纯函数
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, { id: action.id, text: action.text, done: false }];
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
case 'changed': {
return tasks.map(t => t.id === action.task.id ? action.task : t);
}
default: {
throw Error('未知的 action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: '学习 React 官方基础', done: true },
{ id: 1, text: '掌握 Reducer 与 Context', done: true },
{ id: 2, text: '独立构建完整应用', done: false }
];
3. 在外层引入 Provider,底层享受极简代码
现在,打开你的应用入口 App.js:
// App.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function App() {
return (
<TasksProvider>
<h1>每日研习清单</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
看看 App.js 变得多么不可思议的干净!没有一个状态变量,没有一行事件处理函数,没有一层层往下传的 prop!
底层组件如何消费?
在需要添加任务的 AddTask 组件中,直接调用我们导出的自定义 Hook:
// AddTask.js
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';
let nextId = 3;
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch(); // 瞬间获得分发能力!
return (
<>
<input
placeholder="添加新任务"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text
});
}}>
添加
</button>
</>
);
}
在需要展示列表的 TaskList 中:
// TaskList.js
import { useTasks } from './TasksContext.js';
export default function TaskList() {
const tasks = useTasks(); // 瞬间拿到全域数据!
return (
<ul>
{tasks.map(task => (
<li key={task.id}>{task.text}</li>
))}
</ul>
);
}
整个应用的数据层和表现层实现了完美的解耦!任何深层组件想要读数据就用 useTasks(),想要改数据就用 useTasksDispatch()。
总结
- 结合 Reducer(业务逻辑处理)与 Context(全域数据分发)是中大型应用的最佳实践;
- 将 State 与 Dispatch 分别存放在两个 Context 中,实现读写分离并优化性能;
- 封装自定义 Provider(如
TasksProvider)把数据管理逻辑集中在一个模块中; - 导出自定义 Hook(如
useTasks)让消费组件的使用体验达到极致的简洁。