React 中列表操作 reducer 替代多个 useState
FreeGuideOnline
最新
2026-07-06
为什么列表操作不应该使用多个 useState
在 React 中管理列表时,初学者常常会为加载状态、数据、错误、筛选条件等创建多个 useState:
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [filter, setFilter] = useState('all');
这种模式在逻辑简单时看似清晰,但当操作变得复杂(如批量删除、乐观更新、重试机制)时,你会面临严重问题:
- 状态一致性难以保证:一次操作需要同时更新多个状态,忘记重置某个状态会导致界面异常。
- 逻辑分散:状态更新逻辑散落在事件处理函数中,难以复用和测试。
- 性能风险:多个独立的
setState调用可能触发额外的渲染。
useReducer 正是解决这类问题的利器。它将所有相关状态合并到一个对象中,并通过 dispatch 派发动作来管理状态转换。
useReducer 如何替代多个 useState
useReducer 接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 方法。
const [state, dispatch] = useReducer(reducer, initialState);
state:包含列表数据及其所有元状态(加载、错误等)的单一对象。dispatch:用来发送动作对象,由 reducer 根据动作类型返回新状态。
对比传统 useState 模式
| 方面 | 多个 useState |
useReducer |
|---|---|---|
| 状态结构 | 多个独立变量 | 一个聚合对象 |
| 更新逻辑 | 分散在事件处理器中 | 集中在 reducer 函数 |
| 复杂操作一致性 | 手动协调多个 setState | 原子化更新,不会遗漏 |
| 可预测性 | 易产生中间不一致状态 | 每个动作都映射到确定的新状态 |
| 测试 | 难以隔离测试业务逻辑 | reducer 是纯函数,极易测试 |
| 适用场景 | 简单独立状态 | 复杂交互的列表、表单、数据获取 |
实战:用 useReducer 重写列表管理
假设我们要实现一个任务列表,支持获取数据、添加、删除、编辑、切换完成状态、过滤和错误处理。
1. 定义状态结构与动作类型
const initialState = {
data: [],
isLoading: false,
error: null,
filter: 'all',
};
// 动作类型常量(推荐使用常量避免拼写错误)
const ACTIONS = {
FETCH_START: 'FETCH_START',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_ERROR: 'FETCH_ERROR',
ADD_TASK: 'ADD_TASK',
DELETE_TASK: 'DELETE_TASK',
TOGGLE_TASK: 'TOGGLE_TASK',
SET_FILTER: 'SET_FILTER',
};
2. 实现 reducer 函数
reducer 必须是一个纯函数:不修改原状态,根据动作返回新状态。
function taskReducer(state, action) {
switch (action.type) {
case ACTIONS.FETCH_START:
return { ...state, isLoading: true, error: null };
case ACTIONS.FETCH_SUCCESS:
return { ...state, isLoading: false, data: action.payload, error: null };
case ACTIONS.FETCH_ERROR:
return { ...state, isLoading: false, error: action.payload };
case ACTIONS.ADD_TASK:
return { ...state, data: [...state.data, action.payload] };
case ACTIONS.DELETE_TASK:
return {
...state,
data: state.data.filter((task) => task.id !== action.payload),
};
case ACTIONS.TOGGLE_TASK:
return {
...state,
data: state.data.map((task) =>
task.id === action.payload
? { ...task, completed: !task.completed }
: task
),
};
case ACTIONS.SET_FILTER:
return { ...state, filter: action.payload };
default:
return state;
}
}
3. 在组件中使用
import { useReducer, useEffect } from 'react';
function TaskList() {
const [state, dispatch] = useReducer(taskReducer, initialState);
const fetchTasks = async () => {
dispatch({ type: ACTIONS.FETCH_START });
try {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error('网络请求失败');
const data = await response.json();
dispatch({ type: ACTIONS.FETCH_SUCCESS, payload: data });
} catch (err) {
dispatch({ type: ACTIONS.FETCH_ERROR, payload: err.message });
}
};
useEffect(() => {
fetchTasks();
}, []);
// 根据 filter 过滤显示数据
const filteredTasks = state.data.filter((task) => {
if (state.filter === 'completed') return task.completed;
if (state.filter === 'active') return !task.completed;
return true;
});
return (
<div>
<div>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'all' })}>全部</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'active' })}>未完成</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'completed' })}>已完成</button>
</div>
{state.isLoading && <p>加载中...</p>}
{state.error && (
<p>
错误: {state.error}
<button onClick={fetchTasks}>重试</button>
</p>
)}
<ul>
{filteredTasks.map((task) => (
<li key={task.id}>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
{task.text}
</span>
<button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TASK, payload: task.id })}>
{task.completed ? '未完成' : '完成'}
</button>
<button onClick={() => dispatch({ type: ACTIONS.DELETE_TASK, payload: task.id })}>
删除
</button>
</li>
))}
</ul>
<input
type="text"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.target.value.trim()) {
dispatch({
type: ACTIONS.ADD_TASK,
payload: { id: Date.now(), text: e.target.value, completed: false },
});
e.target.value = '';
}
}}
placeholder="添加新任务"
/>
</div>
);
}
进阶技巧:用 useReducer 实现乐观更新
在删除任务时,可以先立即从 UI 中移除该项,再发送 API 请求。如果请求失败,则撤销删除并显示错误。
修改 reducer,添加撤销相关动作:
case ACTIONS.OPTIMISTIC_DELETE:
return {
...state,
data: state.data.filter((task) => task.id !== action.payload.id),
backup: state.data, // 备份旧数据
};
case ACTIONS.ROLLBACK_DELETE:
return {
...state,
data: action.payload,
error: '删除失败,已恢复',
};
组件中的删除处理:
const handleDelete = async (id) => {
dispatch({ type: ACTIONS.OPTIMISTIC_DELETE, payload: { id } });
try {
await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
// 成功则无需额外处理
} catch {
// 失败时恢复数据(需要传入之前备份的数据)
dispatch({ type: ACTIONS.ROLLBACK_DELETE, payload: state.backup });
}
};
何时坚持使用 useState ?
useReducer 虽然强大,但并非所有场景都适用。以下情况用 useState 更合适:
- 状态之间完全独立,没有联动逻辑。
- 状态数量极少(如只有一个布尔开关)。
- 更新逻辑非常直接,不需要维护一致性。
经验法则:如果发现自己在写 setA(…) 的同时还要 setB(…),或者多个状态总是一起变化,那就是切换到 useReducer 的信号。
总结
- 多个
useState容易导致状态不一致和代码碎片化,尤其适合列表管理场景。 useReducer将状态聚合、动作分发和纯函数更新结合,使复杂交互变得可预测。- 初始状态、reducer 函数和组件派发形成了清晰的三层结构:状态定义 → 状态转换 → 视图触发。
- 通过定义常量化动作,可获得更好的代码补全和拼写错误防护。
- 对于网络请求的乐观更新、重试等高级模式,reducer 能轻松保持数据完整性。
把列表操作的状态管理交给 useReducer,你的 React 代码会变得更具可维护性和可测试性。