4.useReducer 相比useState优势体现在哪?

查看飞书原文 ↗3,947 字符
useReducer vs. useState:在复杂场景中寻求更优的状态管理之道​
​
在 React 开发中,useState 是我们管理组件状态最常用的工具之一。它简单、直观,对于处理单个、独立的状态值(如开关状态、输入框内容)非常高效。​
​
但随着应用逻辑变得复杂,我们可能会遇到一些挑战:​
​
•
一个组件内有多个 useState,它们之间相互关联,更新逻辑散落在各处。​
•
下一个状态的值严重依赖于前一个状态,逻辑变得难以追踪。​
•
需要将状态更新的逻辑传递给深层子组件,导致了不必要的函数重建和渲染。​
​
在这些场景下,useReducer 提供了一种更结构化、更可预测的状态管理模式。它并非 useState 的替代品,而是在特定复杂度下更优的解决方案。​
​
从一个实例看差异​
​
我们来看一个常见的计数器场景。如果只是简单的加一,useState 绰绰有余。​
​
const [count, setCount] = useState(0);

// 在组件中直接调用
<button onClick={() => setCount(count + 1)}>Increment</button>
​
现在,我们为这个计数器增加一些复杂度:​
​
1.
点击增加/减少按钮,步长为 1。​
2.
可以从输入框设置一个自定义步长。​
3.
有一个重置按钮,将计数器恢复到初始值 0。​
4.
计数值不能小于 0。​
​
使用 useState,我们可能需要这样实现:​
​
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);

const handleIncrement = () => {
  setCount(c => c + step);
};

const handleDecrement = () => {
  setCount(c => Math.max(0, c - step));
};

const handleReset = () => {
  setCount(0);
};

// ... JSX 中绑定这些函数和 step 输入框
​
可以看到,状态更新的逻辑散落在三个不同的函数中,并且与组件本身紧密耦合。​
​
现在,我们用 useReducer 来重构它:​
​
// 1. 在组件外部或顶部定义 Reducer 函数
const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: Math.max(0, state.count - state.step) };
    case 'set_step':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      throw new Error();
  }
}

// 2. 在组件中使用 useReducer
const [state, dispatch] = useReducer(reducer, initialState);

// 3. 在 JSX 中通过 dispatch 派发动作
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<input 
  type="number" 
  value={state.step}
  onChange={(e) => dispatch({ type: 'set_step', payload: Number(e.target.value) })}
/>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
​
通过这个对比,useReducer 的优势开始显现出来。​
​
useReducer 的核心优势​
​
1. 状态逻辑与视图分离​
​
这是 useReducer 最核心的价值。​
​
•
useState: 状态更新的逻辑(setCount(count + 1))通常直接写在事件处理函数中,与组件的视图渲染代码混合在一起。​
•
useReducer: 所有的更新逻辑都被抽离到 reducer 函数中。组件本身只负责“发出指令”(dispatch({ type: 'increment' })),而不关心状态具体“如何改变”。​
​
这种分离带来了显而易见的好处:​
•
组件更纯粹:组件主要负责 UI 渲染和触发事件,变得更加简洁、易读。​
•
逻辑可复用:reducer 函数是一个纯函数,它可以被轻松地导出、导入,在多个组件间复用。​
•
更易于测试:你可以独立于 React 组件,对 reducer 函数进行单元测试,只需验证输入特定的 state 和 action 是否能得到预期的 newState。​
​
2. 更可预测的状态流与更便捷的调试​
​
当状态逻辑复杂时,追踪 useState 引起的变化会很困难。而 useReducer 遵循了类似 Redux 的模式,让状态变更的路径清晰可见。​
​
每一次状态更新都必须通过 dispatch一个action来触发。action通常是一个包含 type 属性的对象,清晰地描述了“发生了什么”。我们可以轻易地在 reducer 函数或中间件中打印 action 日志,从而得到一个非常清晰的状态变更轨迹,极大地简化了调试过程。​
​
console.log(action) 就像是为你的状态变更安装了一个监控摄像头。​
​
3. 优化深层组件的性能​
​
在复杂的应用中,我们经常需要将状态更新的能力传递给多层嵌套的子组件。​
​
•
使用 useState: 如果我们向下传递 setCount 函数,或者一个包裹了 setCount 的新函数(如 () => setCount(count + 1)),这个函数在父组件每次重新渲染时,其引用地址都可能发生变化。如果子组件使用了 React.memo 进行优化,这种变化会破坏 memo 的效果,导致不必要的重渲染。​
​
•
使用 useReducer: useReducer 返回的 dispatch 函数在组件的整个生命周期内是**引用稳定**的。这意味着你可以放心地将 dispatch 传递给深层子组件。即使父组件重新渲染,dispatch 的引用也不会改变,从而避免了对 React.memo 包裹的子组件产生不必要的渲染。​
​
我们应该如何选择?​
​
理解了各自的优势后,选择的依据就变得清晰了。​
​
​
场景​
推荐使用​
原因​
状态是独立的、简单的值(布尔、数字、字符串)​
useState​
写法最简洁,心智负担最小。​
状态是一个对象或数组,但更新逻辑非常简单​
useState​
依然够用,例如 setForm({ ...form, name: 'new' })。​
多个状态值相互关联、联动更新​
useReducer​
所有更新逻辑收敛在一个 reducer 中,避免逻辑遗漏和不一致。​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
下一个状态依赖于复杂的计算或前一个状态​
​
​
useReducer​
​
​
reducer(state, action) 的模式天然适合这种场景,逻辑清晰。​
​
​
希望将状态逻辑与组件分离,便于测试或复用​
​
​
useReducer​
​
​
reducer 是纯函数,易于独立测试和复用。​
​
​
需要将状态更新方法深层传递给子组件​
​
​
useReducer​
​
​
稳定的 dispatch 函数可以有效配合 React.memo 进行性能优化。​
​
​
构建一个小型状态机​
​
​
useReducer​
​
​
action.type 可以清晰地代表状态机的“事件”,reducer 负责状态转移。​
​
​
​
总结​
​
useReducer 并非 useState 的替代,而是 React 工具箱中一把用于处理复杂状态的“瑞士军刀”。当 useState 让你的组件逻辑开始变得混乱时,往往就是引入 useReducer 的最佳时机。​
​
它通过分离逻辑与视图、提供可预测的状态流以及优化深层传递,帮助我们构建出更健壮、更易于维护的 React 应用。希望通过本文的梳理,我们能够更自信地在 useState 和 useReducer 之间做出最适合当前场景的选择。​
​