3、React中的useReducer:与useState的对比、Reducer模式、Dispatch机制、复杂状态逻辑
聊到状态管理,很多人第一反应就是 useState。确实,它简单、直接,80% 的场景用它就够了。但剩下的 20%,比如表单 wizard、购物车、多步骤流程——你会发现 useState 开始变得力不从心。这时候,useReducer 就该登场了。
我个人习惯把 useReducer 看作是 useState 的「升级版」。它不是要取代谁,而是帮你把复杂的状态逻辑收拢到一个地方。说白了,就是让代码更「讲道理」。
useState vs useReducer:什么时候该换?
先看一个简单的对比表格,帮你快速建立直觉:
| 维度 | useState | useReducer |
|---|---|---|
| 状态结构 | 简单值(string、number、boolean) | 复杂对象(嵌套、多字段) |
| 更新逻辑 | 分散在多个 setState 调用中 | 集中在一个 reducer 函数中 |
| 依赖关系 | 多个状态之间相互影响时难维护 | 通过 action 类型清晰表达意图 |
| 可测试性 | 需要渲染组件才能测试 | 纯函数,直接测 reducer 即可 |
| 学习曲线 | 低,几乎零门槛 | 中等,需要理解 reducer 模式 |
我在项目中遇到过这样一个场景:一个多步骤的注册表单,每一步都有校验、缓存、回退。用 useState 写了七八个状态变量,最后自己都分不清哪个先更新、哪个后更新。后来改成 useReducer,一个 action 搞定一步,逻辑清晰多了。
Reducer 模式:纯函数的力量
Reducer 本质上就是一个纯函数:(state, action) => newState。它接收当前状态和一个描述「发生了什么」的 action,然后返回新的状态。
为什么强调「纯函数」?因为纯函数没有副作用,同样的输入永远得到同样的输出。这意味着你可以放心地写单元测试,不用担心组件渲染环境的影响。
// 一个典型的 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;
}
}
嗯,这里要注意:永远不要直接修改 state。Reducer 必须返回一个新的对象,而不是在原有对象上改属性。这是 React 做浅比较依赖的基础。
return [...state.items, newItem] 而不是 state.items.push(newItem)。
Dispatch 机制:谁触发了更新?
useReducer 返回一个 dispatch 函数,你通过它发送 action 给 reducer。这个 dispatch 是稳定的——它的引用在组件生命周期内不会变,所以不需要放在 useEffect 的依赖数组里。
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
// 发送 action
dispatch({ type: 'INCREMENT' });
dispatch({ type: 'DECREMENT' });
dispatch({ type: 'RESET' });
你想想看,dispatch 其实就是一个「事件发射器」。它不关心 reducer 内部怎么处理,只负责把 action 丢过去。这种解耦让代码更容易理解和维护。
我个人习惯把 action 定义成常量,避免拼写错误:
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
};
// 使用
dispatch({ type: ACTIONS.INCREMENT });
dispatch({ type: 'SET_NAME', payload: '张三' })。这样 reducer 里就能通过 action.payload 拿到数据。
复杂状态逻辑实战:购物车
光说不练假把式。我们来看一个真实场景——购物车。它涉及添加、删除、修改数量、计算总价,状态之间相互依赖,非常适合用 useReducer。
const initialState = {
items: [],
total: 0,
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(item => item.id === action.payload.id);
let newItems;
if (existing) {
newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
newItems = [...state.items, { ...action.payload, quantity: 1 }];
}
return {
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
};
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.id !== action.payload.id);
return {
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
};
}
case 'UPDATE_QUANTITY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
return {
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
};
}
default:
return state;
}
}
看到没?所有逻辑都收拢在 reducer 里。组件只需要调用 dispatch({ type: 'ADD_ITEM', payload: product }),剩下的 reducer 帮你搞定。测试也简单:直接给 reducer 传不同的 action,断言返回的状态对不对。
useReducer 的 SVG 流程图
下面这张图展示了 useReducer 的完整数据流。从组件发起 dispatch,到 reducer 处理,再到状态更新触发重新渲染,一目了然。
什么时候别用 useReducer?
说了这么多好处,也得泼点冷水。不是所有场景都适合 useReducer。
- 状态很简单:比如一个开关、一个输入框的值,用
useState就够了。强行上 reducer 反而增加样板代码。 - 状态更新不频繁:如果只是偶尔改一下,
useState更轻量。 - 团队不熟悉 reducer 模式:如果团队成员都是新手,引入 reducer 可能带来理解成本。这时候可以先用
useState,等逻辑复杂了再重构。
useState 开始,当发现状态逻辑开始「散落一地」时,再迁移到 useReducer。不要一开始就过度设计。
总结一下
useReducer 不是银弹,但它确实是处理复杂状态逻辑的好工具。它让状态更新变得可预测、可测试、可维护。记住三个核心点:
- Reducer 是纯函数——不要修改 state,返回新对象。
- Dispatch 是稳定的——不用担心引用变化导致的问题。
- Action 描述意图——用 type 和 payload 清晰表达「发生了什么」。
下次遇到复杂状态,不妨试试 useReducer。你会发现,代码变得更有条理了。