useReducer 实战:从入门到真香
说实话,我刚接触 React 那会儿,觉得 useState 就是万能药。直到有一次,我接手了一个表单页面——十几个字段,还有联动校验、动态增删行……用 useState 写了三百行,自己都看吐了。后来改用 useReducer,代码量砍了一半,逻辑还清晰了。
今天我们就来聊聊 useReducer。它不是什么高深概念,说白了就是一个「加强版的 useState」。你想想看,当状态逻辑变得复杂时,我们需要一个更结构化的管理方式。
一、useReducer 的基本模式
先看最基础的用法。我习惯把 reducer 想象成一个「状态加工厂」:你给它当前状态和一个动作,它返回一个新状态。
import { useReducer } from 'react';
// 1. 定义 reducer 函数
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
// 2. 在组件中使用
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>当前计数:{state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
核心要点:
reducer必须是纯函数——同样的输入永远得到同样的输出action通常是一个对象,至少包含type字段dispatch是触发状态更新的唯一方式
嗯,这里要注意:reducer 里不要做副作用操作(比如发请求、改 DOM)。这些事应该交给 useEffect 或者事件处理函数去做。
二、useReducer vs useState:什么时候该用谁?
我在项目中遇到过不少同事纠结这个问题。其实判断标准很简单:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 单个独立的值(如开关、输入框内容) | useState | 简单直接,代码量少 |
| 多个关联状态(如表单字段、购物车) | useReducer | 状态之间互相依赖,更新逻辑集中 |
| 状态更新逻辑复杂(多层嵌套、条件分支) | useReducer | 把逻辑抽离到 reducer 中,组件更干净 |
| 状态需要跨组件传递 | useReducer + Context | 比 useState 更容易管理复杂的状态树 |
举个例子。如果你只是控制一个弹窗的显示隐藏,useState 三行搞定:
const [isOpen, setIsOpen] = useState(false);
// 使用:setIsOpen(true)
但如果你要管理一个购物车——添加商品、删除商品、修改数量、计算总价……用 useState 写出来,每个操作都要手动处理状态合并,很容易出 bug。
我的个人习惯:当状态更新逻辑超过 3 个不同的操作类型时,我就会考虑换成 useReducer。这不是硬性规定,但能帮你避免后期重构的痛苦。
三、处理复杂状态逻辑:一个真实案例
我曾经做过一个用户管理面板,需要支持:
- 加载用户列表
- 搜索/筛选
- 分页
- 选中/取消选中
- 批量删除
如果用 useState,大概会写成这样:
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState(new Set());
const [searchTerm, setSearchTerm] = useState('');
然后每个操作都要手动更新多个 state。比如加载成功时:
setLoading(false);
setUsers(data.users);
setError(null);
你想想看,如果漏掉一个 setError(null),之前报错的信息就会一直留在界面上。这种 bug 很难排查。
用 useReducer 重构后,所有逻辑都集中到 reducer 里:
const initialState = {
users: [],
loading: false,
error: null,
page: 1,
selectedIds: new Set(),
searchTerm: '',
};
function userReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, users: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'SET_PAGE':
return { ...state, page: action.payload };
case 'TOGGLE_SELECT':
const newSelected = new Set(state.selectedIds);
if (newSelected.has(action.payload)) {
newSelected.delete(action.payload);
} else {
newSelected.add(action.payload);
}
return { ...state, selectedIds: newSelected };
case 'SET_SEARCH':
return { ...state, searchTerm: action.payload, page: 1 };
default:
return state;
}
}
避坑指南:我曾经在 reducer 里直接修改了 state.selectedIds,结果界面死活不更新。记住:reducer 必须返回一个新对象,不能直接修改 state。像 new Set(state.selectedIds) 这样创建副本是安全的做法。
组件里调用就清爽多了:
const [state, dispatch] = useReducer(userReducer, initialState);
// 加载用户
dispatch({ type: 'FETCH_START' });
try {
const data = await fetchUsers(state.page, state.searchTerm);
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'FETCH_ERROR', payload: err.message });
}
// 切换选中
dispatch({ type: 'TOGGLE_SELECT', payload: userId });
你看,每个操作对应一个 action,逻辑一目了然。而且如果以后要加「全选」「取消全选」功能,只需要在 reducer 里加两个 case 就行,组件代码几乎不用改。
四、进阶技巧:action 携带更多信息
action 不一定只有 type。我经常在 action 里放 payload、meta 等字段:
dispatch({
type: 'UPDATE_USER',
payload: { id: 1, name: '张三' },
meta: { timestamp: Date.now() }
});
这样在 reducer 里可以拿到完整的信息来做更精细的控制。比如根据时间戳决定是否忽略过期请求。
总结一下:
- useReducer 不是 useState 的替代品,而是互补品
- 当状态逻辑复杂到让你头疼时,就是 useReducer 出场的时候
- reducer 让状态更新变得可预测、可测试、可维护
最后说一句:别为了用而用。如果你的组件就两三个状态,用 useState 完全没问题。但一旦发现状态之间「牵一发而动全身」,或者更新逻辑里充满了 if-else 和临时变量,嗯,是时候考虑 useReducer 了。