React Reduce
1. 引入
在 React 中,useState
是处理组件状态最常用的 Hook。但随着组件逻辑变得复杂,状态更新散布在多个事件处理函数中,会导致组件变得臃肿且难以维护。
想象一个管理任务列表的组件,它需要处理添加、删除和编辑任务。如果使用 useState
,代码可能如下:
function TaskManager() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text: text, done: false }]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => t.id === task.id ? task : t));
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter(t => t.id !== taskId));
}
// ...
}
当状态更新逻辑越来越多时,这些函数会让组件本身变得非常庞大。所有这些 setTasks
调用都分散在不同的事件处理器中,使得追踪状态变化变得困难。
useReducer
为我们提供了一种更强大、更结构化的方式来管理复杂的状态。
2. Reducer 模式
基本概念
Reducer 模式旨在将**“发生了什么”(用户的操作)与“状态如何更新”**(业务逻辑)分离开来。
Reducer 是一个纯函数,它接收两个参数:当前的状态 (state) 和一个 action 对象,然后返回下一个新的状态。
(currentState, action) => nextState
Action
Action 是一个普通的 JavaScript 对象,用于描述用户执行的操作。按照约定,它通常包含一个 type
属性(一个字符串,描述操作类型)和一个可选的 payload
属性(携带操作所需的数据)。
// 一个描述“添加任务”的 action
{ type: 'added_task', payload: { text: '学习 useReducer' } }
// 一个描述“删除任务”的 action
{ type: 'deleted_task', payload: { id: 123 } }
Reducer 工作流程
Reducer 的工作流程总体包含三步:Dispatch → Reducer → New State。具体如下:
- Dispatch (分发): 在组件中,当用户执行某个操作时(如点击按钮),会调用一个名为
dispatch
的函数,并将一个action
对象传递给它。 - Reducer (处理): React 接收到
dispatch
的action
后,会调用提供的reducer
函数,并将当前的state
和这个action
作为参数传入。 - New State (新状态):
reducer
函数根据action.type
执行相应的逻辑,计算并返回一个新的状态。 - Re-render (重渲染): React 检测到状态已更新,会使用新的状态重新渲染组件及其子组件。
Reducer 模式的优点
Reducer 模式具有如下的优点:
- 集中化与可预测性: 所有状态更新逻辑都集中在 reducer 中,使得状态的流转清晰、可预测,极大地简化了调试过程。
- 提升可测试性: Reducer 是纯函数,不依赖 React,可以被独立地导出和测试。你可以轻松地为每种 action 编写单元测试,确保逻辑的正确性。
- 优化深层组件的性能:
dispatch
函数的身份在组件的整个生命周期内是稳定不变的。当你通过 props 或 Context 将dispatch
传递给深层子组件时,它不会像useState
的setState
函数(除非用useCallback
包裹)那样在每次重渲染时都创建新的函数,从而可以避免子组件不必要的重渲染。 - 代码分离: 你可以将 reducer 逻辑完全移出组件文件,使组件代码更专注于视图,更加简洁。
3. useReducer
Hook
基本概念
useReducer
是 React 提供的内置 Hook,用于在组件中使用 reducer 模式。
useReducer
接收一个 reducer 函数和一个初始状态,返回一个包含当前状态和 dispatch
函数的数组。
const [state, dispatch] = useReducer(reducer, initialState);
使用 useReducer
重构
让我们将前面的 TaskManager
组件重构为使用 useReducer
。
- 编写 Reducer 函数:将所有状态逻辑整合到一个 reducer 函数中。通常使用
switch
语句来处理不同的action.type
。
// reducers/taskReducer.js
export function taskReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, { id: action.id, text: action.text, done: false }];
}
case 'changed': {
return tasks.map(t => t.id === action.task.id ? action.task : t);
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw new Error('未知的 action 类型: ' + action.type);
}
}
}
- 在组件中使用
useReducer
:用useReducer
替换useState
,并用dispatch(action)
替换所有的setTasks
调用。
// TaskManager.js
import { useReducer } from 'react';
import { taskReducer } from './reducers/taskReducer';
let nextId = 3;
const initialTasks = [/* ... */];
function TaskManager() {
const [tasks, dispatch] = useReducer(taskReducer, initialTasks);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({ type: 'deleted', id: taskId });
}
// ...
}
现在,组件本身只负责“分发指令”,而不再关心“状态如何具体更新”。
惰性初始化
如果初始状态的计算成本很高,useReducer
允许我们传入第三个参数:一个 init
函数。这样,初始状态只会在组件初次渲染时计算一次。
function createInitialState(username) {
// ... 执行一些昂贵的计算 ...
return { user: username, tasks: [] };
}
// React 将会调用 createInitialState('Taylor') 来获取初始状态
const [state, dispatch] = useReducer(reducer, 'Taylor', createInitialState);
4. Action Creator 模式
随着应用变得复杂,直接在组件中编写 action
对象(如 { type: 'added', ... }
)会带来一些问题:
- 重复劳动: 如果多个地方都需要添加任务,你就需要多次编写相同的
action
对象结构。 - 容易出错: 手动编写
type
字符串和payload
结构,很容易因拼写错误或结构遗漏而出错。 - 关注点耦合: 组件被迫了解
action
的内部结构,这违反了“逻辑与视图分离”的原则。
Action Creator 模式就是为了解决这些问题而生的。它是一个简单的函数,其唯一目的就是创建并返回一个 action
对象。
一个 Action Creator 的简单示例如下:
// actionCreators.js
// addTask 是一个 Action Creator
export function addTask(text) {
return {
type: 'added',
payload: { text: text }
};
}
// deleteTask 是另一个 Action Creator
export function deleteTask(taskId) {
return {
type: 'deleted',
payload: { id: taskId }
};
}
使用它之后,组件中的 dispatch
调用就变得非常清晰和声明式:
import { addTask, deleteTask } from './actionCreators';
// ... 在组件内部
dispatch(addTask('学习 Action Creator'));
dispatch(deleteTask(123));
Action Creator 模式带来了下面的好处:
- 彻底解耦: 组件现在只需调用一个函数,完全无需关心
action
的type
是什么,payload
的结构是怎样的。 - 简化与复用: 组件的调用代码变得极其干净和易读。Action Creator 可以在应用中的任何地方被复用。
- 易于维护: 如果未来需要修改某个
action
的结构(比如为“添加任务”增加一个priority
属性),你只需要在对应的 Action Creator 函数中修改一处即可,所有使用它的组件都会自动生效。 - 逻辑内聚: 我们可以将一些辅助逻辑(如 ID 生成)也封装起来。例如,我们可以改进 Reducer,让它自己管理 ID,这样 Action Creator 甚至都不需要传递 ID:
// reducers/taskReducer.js (改进后)
let nextId = 3; // ID 生成逻辑内聚在 reducer 模块中
export function taskReducer(tasks, action) {
switch (action.type) {
case 'added': {
// Action Creator 只需提供 text
return [...tasks, { id: nextId++, text: action.text, done: false }];
}
// ... 其他 case
}
}
5. useReducer
+ useContext
当需要管理跨多个组件的全局状态时,useReducer
和 useContext
是一对黄金搭档。结合上一章的 Action Creator 模式,我们可以构建出一套不依赖第三方库、兼具结构清晰和高性能的轻量级全局状态管理方案。
设计目标
- 逻辑与视图分离: 状态更新逻辑(Reducer)与 Action 创建逻辑(Action Creators)应与 UI 组件完全解耦。
- 全局访问: 应用中任何层级的组件都可以轻松地读取状态或触发状态更新,而无需通过繁琐的 props 钻探。
- 性能优化: 一个组件应该只在其真正关心的数据发生变化时才重渲染。特别是,一个只负责触发更新(调用 Action Creators)的组件,不应该因为状态的改变而重渲染。
具体实现
我们将通过一个任务管理器的例子,一步步构建这个模式。
- 定义 Reducer (业务逻辑核心):我们使用在上一章改进过的 Reducer,它自己管理 ID 的生成。
// reducers/taskReducer.js
let nextId = 3;
export function taskReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, { id: nextId++, text: action.text, done: false }];
}
case 'changed': {
return tasks.map(t => t.id === action.task.id ? action.task : t);
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw new Error('未知的 action 类型: ' + action.type);
}
}
}
- 创建两个独立的 Context:这是性能优化的关键,我们创建下面的 Context:
- 一个 Context 用于传递状态 (state) 本身。
- 另一个 Context 用于传递封装后的 Action Creators。
// contexts/TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null); // 用于 state
export const TasksDispatchContext = createContext(null); // 用于 actions
- 封装 Provider 和 Action Creators:我们创建一个 Provider 组件,它内部调用
useReducer
,然后创建一组与dispatch
绑定的 Action Creator 函数。为了性能,这个函数对象通过useMemo
进行缓存。
// contexts/TasksContext.js (续)
import { useReducer, useMemo, useContext } from 'react';
import { taskReducer } from '../reducers/taskReducer';
const initialTasks = [
{ id: 0, text: '参观卡夫卡博物馆', done: true },
{ id: 1, text: '看木偶戏', done: false },
{ id: 2, text: '列侬墙', done: false },
];
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(taskReducer, initialTasks);
// 使用 useMemo 封装 Action Creators,保证其引用稳定
const actions = useMemo(() => ({
addTask: (text) => {
dispatch({ type: 'added', text: text });
},
changeTask: (task) => {
dispatch({ type: 'changed', task: task });
},
deleteTask: (taskId) => {
dispatch({ type: 'deleted', id: taskId });
}
}), [dispatch]); // dispatch 的引用是永久稳定的
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={actions}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
- 创建自定义消费 Hooks:使用自定义 Hook 让组件消费 Context 更方便,并包含健壮性检查。
// contexts/TasksContext.js (续)
export function useTasks() {
return useContext(TasksContext);
}
export function useTaskActions() {
return useContext(TasksDispatchContext);
}
使用组件
现在,组件可以从 Context 中获取状态和已经封装好的 Action,代码变得非常简洁。下面是一些使用的例子:
AddTask.js
:只消费useTaskActions
,因此在状态变化时不会重渲染。
import { useState } from 'react';
import { useTaskActions } from './contexts/TasksContext';
export default function AddTask() {
const [text, setText] = useState('');
const { addTask } = useTaskActions(); // 获取封装好的 addTask 函数
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => {
setText('');
addTask(text); // 直接调用,无需关心实现细节
}}>添加</button>
</>
);
}
TaskList.js
和Task.js
:TaskList
消费useTasks
来渲染列表,Task
组件消费useTaskActions
来处理单个任务的变更和删除。
// TaskList.js
import { useTasks } from './contexts/TasksContext';
import Task from './Task';
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
// Task.js
import { useState } from 'react';
import { useTaskActions } from './contexts/TasksContext';
export default function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const { changeTask, deleteTask } = useTaskActions();
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => changeTask({ ...task, done: e.target.checked })}
/>
{isEditing ? (
<>
<input
value={task.text}
onChange={e => changeTask({ ...task, text: e.target.value })}
/>
<button onClick={() => setIsEditing(false)}>保存</button>
</>
) : (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>编辑</button>
</>
)}
<button onClick={() => deleteTask(task.id)}>删除</button>
</label>
);
}