目录#
简介#
本文翻译自 Should I useState or useReducer?,原文作者为 Kent C. Dodds。
本文通过两个个示例讲解了useState和useReducer的适用场景,同时介绍了一个useReducer的简单用法。
另外文末将会附上作者原文中讲解代码的视频。
前言#
useState
和 useReducer
这两个用于处理状态的React hooks,我们应该使用哪一个呢?
每当有两个选项来处理同一件事时,人们常常提出这样的疑问:『我应该选择哪一个呢?』
有两种可能性可以来解释我们为什么会有多种方法用于做同一件事:
- 其中一个方法是传统的做法,另一些方法是对传统方法的改进,是『新事物』。具体来说,我们通常为了向后兼容,会保留原有的旧方法,而新方法则是更新迭代所需要的新的基础设施。例如:React的 Class组件(旧方法) vs 函数组件(新方法)
- 它们侧重于不同的角度,应该根据需求进行分析与考虑,从而分别应用于适合它们的情况(有时这也意味着在一段程序中会使用到上述提及的多个方法);例如:
useState
vsuseReducer
或是 Static vs Unit vs Integration vs E2E tests,亦或是 “Control Props” vs “State Reducers”
useState
和 useReducer
的对比,符合上面的第二条,所以让我们一起来讨论一下它们各自的适用场景吧~
(悄悄说一句,这不是一个很难回答的问题 😂)
我们举例来说吧#
我认为讨论适用场景的最好办法就是通过实际例子来进行分析,我们将集中关注于两个典型例子,一个更适合 useState
,另一个更适合 useReducer
。这两个例子并不能覆盖所有的场景,但仅作为抛砖引玉,希望你能从这得到启发,形成你自己的判断。
一个自定义hook: useDarkMode
#
我最近为我的教程项目编写了这段代码,看起来蛮有趣的,让我们来对比一下 useState
和 useReducer
的不同实现吧。
useState
的版本#
1 | function useDarkMode() { |
这段代码描述的是:我们根据用户所设定的系统主题颜色,来选择将网页初始化为深色模式或是日常模式,并将返回 mode
和 setMode
,随着主题颜色的改变(不论是直接调用 setMode
,或是用户直接修改了系统主题色),我们都会把修改后的值存入 localStorage
以便于后续开发;
useReducer
的版本#
有几种 useReducer
的用法可以重构上面那一段代码,我从我们大部分人所熟知的写法讲起:
1 | const preferDarkQuery = '(prefers-color-scheme: dark)' |
wow!单凭前后两端代码,我们应该都会认为 useState
的版本是更简洁的写法。但是!我们可以通过放弃这种典型 redux 风格细粒度处理来极大地简化我们的 reducer,例如这样:
1 | function useDarkMode() { |
这样看起来好多了。但即使是大幅度精简了,useState
版本仍然是更加简介清晰的,所以在这种场景下,useState
更为适用。
当你所管理的这个状态,描述的是一个独立元素时,你应该使用
useState
另一个自定义hook: useUndo
#
Github上的 useUndo
是一个优秀的package,我从中受到了启发,感谢你,Homer Chen!
(每个状态都是通过state描述的,所以没有下划高亮噢…)
useState
的版本#
1 | function useUndo(initialPresent) { |
看起来还不错对吧?实际上,在某些情况下,这却可能导致运行异常;但在解释何时会发生异常之前,我想解释一下大家对于按顺序调用多个状态更新函数的常见误解(正如我们在上面这些函数中所调用的形式)。
通常我们会认为 setState
函数每调用一次,就会触发一次重新渲染,所以 reset
函数会触发三次重新渲染。那么,首先要注意的是: (在优化重新渲染环节之前,应该先修复渲染缓慢的函数)[https://kentcdodds.com/blog/fix-the-slow-render-before-you-fix-the-re-render];其次,React有着合并更新系统,所以当你从事件回调或是 useEffect
钩子函数中执行 reset
函数时,这三次set将被合并,最终只会执行一次重新渲染;
尽管如此,但当我们从一个 async 函数(例如一个HTTP请求函数)中调用 reset
时,依然会导致三次重新渲染;但在未来 React concurrent 模式中,这个问题将会被优化;所以我的主要关注点不在于重新渲染上。
我的关注点是代码中隐藏的闭包旧值的bug!你能认出他们吗?有三个!我给你们一个提示,undo
,redo
和 set
中都有一个,但 reset
中没有。
下面是一个复现Bug的案例:
1 | function Example() { |
实际上是:
1 | { |
而预想的打印结果应该是:
1 | { |
所以在这种情况下,second
发生了啥?oh!看起来是我们忘了将 set
放进 useEffect 的依赖数组了,淦,我们把它加上试试:
1 | function Example() { |
修改完毕,保存,重新加载… 等一下?!输出看起来就有问题!
1 | { |
但我们不是已经把 set
函数 memo化 过了吗?它不应该在它自身依赖未改变的情况下发生改变的呀… oh!是因为它自身包含了 past
和 present
两个值,所以每当我们调用一次 set
,这两个值就会发生改变,这就导致了无限循环!
现在这些例子都是人为拟定的,但是如果你根据网络请求的回调来更新状态,并且它们的返回顺序与发送时不同,那么可能会出现类似的错误。不管怎样,你都不想去思考这种事情,对吧?
我们可以通过 useReducer
来解决这个问题,但我们也可以通过修改 useState
的使用方式,来避免这个问题,如下:
1 | function useUndo(initialPresent) { |
我做了以下内容来修复这个问题:
- 我使用了 setState 的回调形式来进行状态更新,这样的好处是我能从回调函数的参数中获取到先前的 state, 这样我就不需要将 state 作为依赖,并且避免闭包旧值问题;
- 我将所有 state 通过一个对象整合在了一起,这样的必要性是:我需要通过另一个状态来判定当前状态的值,例如在
redo
函数中,我们需要通过present
来更新past
,future
来更新present
; - 我直接计算组合获得
canUndo
和canRedo
这两个状态值,所以我不需要将其列入依赖列表;
这些步骤能让我们解决掉这个先前复现出的Bug,看起来不错,接下来让我们看看在 useResucer
中会是什么样的:
useResucer
的版本#
1 | const UNDO = 'UNDO' |
useUndo
现在看起来很简洁了。如果我们从一开始就使用 useReducer,我们甚至不需要考虑向依赖项数组添加什么东西,因为这些函数非常简单,它们不需要任何的依赖。所有的逻辑都存在于我们的 reducer 中。这让我们很自然地规避掉闭包旧值这个问题。
当 state 中的一个元素依赖于另一个 state 的值来进行更新时,请使用
useReducer
总结#
当你需要一些 『既定规则』 时(不是ESLINT的规则哦),请参考以下几条:
- 当 state 只是用来描述一个独立的元素时,请使用
useState
- 当 state 中的一个元素依赖于另一个 state 的值来进行更新时,请使用
useReducer
除开这些 『既定规则』,如何写更好是一件非常主观的事情,当然,老实说,即使是 『既定规则』 也是主观的,因为正如我所展示的代码,你可以使用任一方法来完成你想做的所有事情。
另外,请注意,这也是视情况而定的。 你可以在一个 hook 或 组件中同时使用多个 useState
或 useReducer
。 按照使用作用域分离 state,如果它们一起更新,最好将其放在 reducer 中;如果某项 state 与该hook/组件中的其他 state 来说非常独立,那么将其与其他 state 绑定在一起只会给 reducer 增加不必要的复杂性,最好就让它保持 useState
的形式。
因此,这不仅是 『当 useStates
数量超过X个时,我就要切换到 useReducer
』这样的对比,而是存在着比这更细微的差别。 希望这篇文章可以帮助你了解到这些细微差别具体是什么,并找到可以最适合你目前情况的使用方法。 通常,我建议从 useState
开始,并留心 state 需要合并更改时迁移到 useReducer
。
感谢!遥祝各位写码愉快!
原文视频#
Watch “When to useState instead of useReducer” on egghead.io
Watch “When to useReducer instead of useState” on egghead.io