尽量使用 useReducer,不要使用 useState

10,071 阅读3分钟

原文:useReducer, don't useState

本文难度:入门级别

本文默认你已经大概了解过 React Hooks,如果不了解可以先看看 ReactJS 的文档

当开发者们开始在他们的应用中使用 React Hooks API 时,很多人一开始都会把 useState 作为他们的状态管理工具。 然而,我强烈认为 useReducer 比 useState 更适合做状态管理。

首先我来定义一下『更适合』是什么意思:

  • 更容易管理大量状态
  • 更容易被其他开发者理解
  • 更容易被测试

接下来我分别对三点进行阐述。

管理大量状态

这篇文章大部分观点只是我的主观看法。

useState 有一个与 class 组件里面的 setState 明显不同的地方,那就是 useState 不对状态做浅层合并了,而 useReducer 会合并。

为了说明这一点,我这里给一个使用 useReducer 来实现『撤销/重做』的例子:

function init(initialState) {
  return {
    past: [],
    present: initialState,
    future: [],
  }
}
function reducer(state, action) {
  const { past, future, present } = state
  switch (action.type) {
    case 'UNDO':
      const previous = past[past.length - 1]
      const newPast = past.slice(0, past.length - 1)
      return {
        past: newPast,
        present: previous,
        future: [present, ...future],
      }
    case 'REDO':
      const next = future[0]
      const newFuture = future.slice(1)
      return {
        past: [...past, present],
        present: next,
        future: newFuture,
      }
    default:
      return state
  }
}

用 useState 达到相同的效果有点困难,不过也并不是不可能。我只是想告诉里使用 useReducer 是多么地方便,这也引出了第二点。

译注:如果用 useState 来做,只需要把 past / present / future 放到同一个 state 里面即可,但是会造成代码分散。而且 useState 也不推荐你在一个 state 里放太多东西,因为它不会合并 state,用起来不方便。

更容易被其他开发者理解

在 Web 开发中,我们面对的问题很多时候并不是纯技术问题。大部分时候你都要跟其他开发者工作,他们的开发经验很可能跟你的很不一样。

由于大部分前端开发者都了解过 Redux,所以使用 useReducer 比使用 useState 更能快速获得收益。其核心概念比如 diapatch 一个 action,使用 reducer 来更新 state,都比 useState 更容易被这些开发者掌握。

还有一点值得注意,那就是即使你目前是一个人在开发一个应用,你也保不齐以后会有其他人接手这份代码。

更容易被测试

要论 Hooks RFC、Twitter 里被讨论最多的话题,那就是如何测试 Hooks。我觉得要让开发者理解测试 Hooks 的最佳实践,还是要花费一些时间的(译注:尤其是 useState)。但是如果你使用的是 useReducer,那么你所有的跟 state 相关的业务逻辑代码都可以放到一个单独的函数里,跟你的组件分开,非常好测试。

把状态更新代码和渲染逻辑分开,使得你可以把测试代码也分成这两部分。以上面的 reducer 代码为例, 我们可以轻松地测试撤销和重做,做法是把 mock 状态和 action 传给 reducer 即可,我们甚至不用引入 React!

test('it supports undoing the state', () => {
  const state = { past: [{ count: 0 }], present: { count: 1 }, future: [] }
  const newState = reducer(state, { type: 'UNDO' })
  expect(newState.present.count).toBe(0)
})

总结

我并不期望大家只使用 useReducer 不使用 useState,我个人也不会这么做,它们各有各的使用场景。但是我的确认为 useReducer 在复杂的状态管理场景下比 useState 更好维护。