自定义Hook中useState与useReducer的区别及底层实现探究
你的理解在多数业务场景下是成立的,但二者并非完全等价,底层实现和细节上存在一些差异,我来拆解清楚:
首先看你给出的示例代码:
核心更新逻辑与初始状态
const update = (state, action) => { switch (action) { case 'increment': return state + 1 case 'decrement': return state - 1 default: return state } } const initState = 0
基于 useReducer 的自定义 Hook
const useWithReducer = () => { const [state, dispatch] = useReducer(update, initState) return [state, dispatch] }
最初的 useState 版本(存在隐患)
const useWithState = () => { const [state, setState] = useState(initState) const dispatch = (action) => setState(update(state, action)) return [state, dispatch] }
这个版本有明显问题:dispatch里的state是闭包捕获的旧值,如果组件重渲染后调用这个dispatch,会导致状态更新基于过期值计算,出现逻辑错误。
改进后的 useState 版本(和 useReducer 行为一致)
const useWithState = () => { const [state, setState] = useState(initState) const dispatch = (action) => setState((currentState) => update(currentState, action)) return [state, dispatch] }
这个版本用了函数式更新,确保每次拿到的是最新的currentState,此时它的功能表现和useWithReducer几乎完全一致。
二者的核心差异
dispatch 的稳定性
useReducer 返回的dispatch引用是完全稳定的,不会随组件重渲染改变;而 useState 的setState虽然本身稳定,但你自己封装的dispatch(比如上面useWithState里的dispatch),如果不用useCallback包裹,每次重渲染都会生成新的函数,可能会触发不必要的子组件重渲染。初始化逻辑的灵活性
useReducer 支持第三个参数(初始化函数),比如useReducer(update, initState, (initial) => initial * 2),这个初始化函数只会执行一次;而 useState 要实现同样的效果,需要写成useState(() => initFn()),虽然功能等价,但 useReducer 的写法更明确,适合复杂的初始化逻辑(比如从props计算初始状态)。复杂状态逻辑的维护性
当状态更新逻辑涉及多个 action 类型、依赖多个状态值,或者需要拆分逻辑时,useReducer 把所有更新逻辑集中在 reducer 函数里,更容易维护和测试;而 useState 用函数式更新的话,逻辑会分散在各个 setState 调用中,代码复杂度高了之后会变得混乱。底层实现细节
React 内部中,useState 其实是基于 useReducer 的一个简化实现(默认 reducer 就是直接替换状态),但 useReducer 有专门的调度逻辑,比如处理 reducer 的执行时机、对状态更新的批量处理细节上,和 useState 存在细微差别,但这些差异在业务代码层面几乎感知不到。
总结
你改进后的useWithState和useWithReducer在功能表现上是等价的,但并非底层实现完全无差异。在简单状态场景下,用 useState 模拟 useReducer 完全可行;但当状态逻辑复杂时,useReducer 的写法会更清晰、更易维护。
内容的提问来源于stack exchange,提问作者ellis hogan

