当组件的状态逻辑变得复杂——比如涉及多个子值、相互依赖的状态更新,或者下一次状态依赖于上一次状态的值——useState 的分散声明和更新函数会让代码变得难以追踪。useReducer 是 React 为这类场景提供的替代方案,它将状态更新逻辑集中到一个名为 reducer 的纯函数中。
useReducer 与 useState 的关系
可以把 useReducer 理解为 useState 的“升级版”。useState 适合简单的独立状态,useReducer 适合状态之间有关联、更新逻辑复杂的场景。实际上,useState 的底层就是用 useReducer 实现的。
两者的核心区别在于状态更新方式:
useState:直接调用 setter 传入新值。useReducer:通过dispatch派发一个 action 对象,由 reducer 函数根据 action 类型计算新状态。
基本语法
const [state, dispatch] = useReducer(reducer, initialState, init?);
reducer:一个纯函数(state, action) => newState,接收当前状态和一个 action 对象,返回计算后的新状态。initialState:初始状态值。init(可选):惰性初始化函数,用于计算初始状态,避免在每次渲染时重复创建。- 返回值:当前状态
state和一个dispatch函数,用来派发 action。
典型的 reducer 函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
throw new Error('Unknown action type');
}
}
Action 通常是一个包含 type 字段的对象,可以携带额外的数据放在 payload 中。约定俗成的模式来自于 Redux,但 React 本身对 action 的形状没有强制要求。
何时使用 useReducer
适用场景:
- 状态逻辑复杂:一个操作会同时改变多个状态字段,或者不同的操作以不同的方式改变同一状态。
- 下次状态依赖上次状态:比如计数器、多步骤表单、购物车总价计算等。
- 状态更新逻辑需要被复用:reducer 是纯函数,可以单独提取、测试和复用。
- 需要清晰追踪状态变更:通过 action 类型可以明确知道是什么触发了变化,便于调试和维护。
不适用场景:
- 简单的独立状态(如一个布尔 toggle、一个输入框的值),直接使用
useState即可,引入 reducer 反而增加样板代码。
实战示例:购物车状态管理
// 定义 action 类型常量(非必须,但便于维护)
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const CLEAR_CART = 'CLEAR_CART';
const initialCart = [];
function cartReducer(state, action) {
switch (action.type) {
case ADD_ITEM: {
const existingItem = state.find(item => item.id === action.payload.id);
if (existingItem) {
return state.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...state, { ...action.payload, quantity: 1 }];
}
case REMOVE_ITEM:
return state.filter(item => item.id !== action.payload);
case CLEAR_CART:
return [];
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, initialCart);
const addItem = (product) => {
dispatch({ type: ADD_ITEM, payload: product });
};
const removeItem = (productId) => {
dispatch({ type: REMOVE_ITEM, payload: productId });
};
const clearCart = () => {
dispatch({ type: CLEAR_CART });
};
return (
<div>
<h2>购物车 ({cart.length} 种商品)</h2>
<ul>
{cart.map(item => (
<li key={item.id}>
{item.name} x {item.quantity}
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
<button onClick={clearCart}>清空购物车</button>
{/* 模拟添加商品 */}
<button onClick={() => addItem({ id: 1, name: 'React 实战', price: 99 })}>
添加《React 实战》
</button>
</div>
);
}
在这个例子中,所有的购物车逻辑(添加、去重、累加、删除、清空)都集中在 cartReducer 中,组件里只需调用 dispatch 派发对应的 action,状态变更逻辑清晰可预测。如果将来添加新的操作(如修改数量),只需在 reducer 中增加一个 case,不会影响组件的其他部分。
惰性初始化
当初始状态的创建成本较高(例如需要从 localStorage 读取或进行复杂计算),可以使用第三个参数 init 函数进行惰性初始化,避免每次渲染都重新计算:
function init(initialValue) {
// 从本地存储读取,或进行其他计算
const savedCart = localStorage.getItem('cart');
return savedCart ? JSON.parse(savedCart) : initialValue;
}
const [cart, dispatch] = useReducer(cartReducer, initialCart, init);
init 接收 initialCart 作为参数,仅在首次渲染时调用一次。
与 useContext 结合:轻量级全局状态管理
useReducer 和 useContext 结合,可以实现无需第三方库的全局状态管理,非常适合中小型应用。
// CartContext.jsx
const CartContext = createContext();
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, initialCart);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart 必须在 CartProvider 内部使用');
}
return context;
}
任何被 CartProvider 包裹的组件都可以通过 useCart() 获取 cart 和 dispatch,实现跨层级的购物车状态共享。这种方式比引入 Redux 更加轻量,且逻辑依然清晰可控。
useReducer 的优势
- 可预测性:状态更新逻辑集中在 reducer 中,遵循
(state, action) => newState的模式,易于推理。 - 可测试性:reducer 是纯函数,测试时只需提供当前状态和 action,断言返回的新状态。
- 可维护性:当状态更新逻辑逐渐膨胀时,分散的多个
useState和setState调用容易失控,而 reducer 将所有分支集中管理,甚至可按业务拆分多个 reducer 再合并。 - 调试友好:可以轻松记录每次 dispatch 的 action 和前后的 state,配合 Redux DevTools 也可以进行时间旅行调试(需要额外桥接)。
注意事项
- reducer 必须是纯函数:不能修改原 state,必须返回一个新对象;不能在 reducer 中执行副作用(如请求数据、设置定时器)。副作用应该放在组件或在
dispatch之前处理。 - action 设计不要过度抽象:虽然 action 可以是一个字符串,但推荐使用具有描述性的字符串常量或更结构化的对象,保持代码可读性。
- 不要将所有状态都放进一个 reducer:可以采用多个
useReducer,每个 reducer 管理一类状态,保持 reducer 函数的简洁和专注。
总结
useReducer 是 React 内置的解决复杂状态管理的利器。当 useState 开始导致状态更新逻辑四散各处、难以追踪时,就是引入 useReducer 的明确信号。它用 reducer 函数集中管理状态变更规则,让组件代码回归“触发意图(dispatch)”和“渲染状态(state)”的清晰分离,是迈向更可维护状态管理的第一步。