精读React hooks(二):全面掌握useReducer
上一篇文章中,我们学习了useState
的一些基础用法和进阶技巧,useState
是React的一个基础Hook,允许我们在函数组件中存储状态。
随着应用逐渐复杂,我们经常发现useState
在管理复杂的状态逻辑时显得有些力不从心。这时,React为我们提供的另一个更为强大的hook——useReducer
——可以帮助我们优雅地处理复杂状态。
useReducer
允许我们使用 action 和 reducer 的方式来组织复杂的状态逻辑,使其变得更加清晰和模块化,弥补了useState
的局限性。
基础用法
与useState
相似,useReducer
也是 React 的 Hook,而且也只能放在组件最顶层使用。与前者不同的地方在于,它是通过 action 来更新状态的,使状态更新逻辑更具可读性。
useReducer
接收三个参数:
- reducer 函数:指定如何更新状态的还原函数,它必须是纯函数,以 state 和 dispatch 为参数,并返回下一个状态。
- 初始状态:初始状态的计算值。
- (可选的)初始化参数:用于返回初始状态。如果未指定,初始状态将设置为 initialArg;如果有指定,初始状态将被设置为调用
init(initialArg)
的结果。
useReducer
返回两个参数:
- 当前的状态:当前状态。在第一次渲染时,它会被设置为
init(initialArg)
或 initialArg(如果没有 init 的情况下)。 - dispatch:调度函数,用于调用 reducer 函数,以更新状态并触发重新渲染。
基本形式如下:
通常情况下,我们只会用到useReducer
的前两个参数,如这个计数器组件:
使用dispatch
的注意事项
-
dispatch
调用后,状态更新是异步的,因此立刻读取状态可能仍是旧的。 -
React 对
dispatch
有一个优化机制:如果dispatch
触发更新前后的值相等(使用Object.is判断),实际上React不会进行重新渲染,这是出于性能考虑。
使用reducer
函数的注意事项
你在reducer
里面更新对象和数组的状态,需要创建一个新的对象或数组,而不是在原对象和数组上修改,这一点和useState
是一样的。
初始化状态:使用init
函数
上一节我们提到了useReducer
还有第三个参数init
,那么它的作用是什么?它也是为了性能优化而来。
我们先假设一个场景,计数器的值保存在localStorage
里面,进入页面的时候,我们希望从localStorage
中读取值来作为useReducer
初值,如果没有init
,我们可以这样做:
在这个例子中,我们直接调用getInitialCount
函数作为useReducer
的第二个参数,从而得到初始状态。当React初始化这个组件时,它会执行这个函数并使用其返回值作为初始状态。
如果在第三个参数里进行初始化,代码是这样写:
这两种方式看似差不多,但它们区别很大:
- 执行时机:
- 直接调用函数作为第二个参数:这个函数会在每次组件渲染时执行。
- 使用
init
函数:init
函数只在组件初次渲染时执行一次。
- 访问到的数据:
- 直接调用函数作为第二个参数:这个函数只能访问到定义它时的作用域内的数据。
- 使用
init
函数:由于init
函数接受initialArg
作为参数,这使得init
函数具有更大的灵活性,能够基于传入的参数进行计算。
- 代码组织:
- 直接调用函数作为第二个参数:这通常更简洁,适合那些简单的初始化逻辑。
- 使用
init
函数:它提供了更清晰的代码组织结构,特别是当初始化逻辑相对复杂或需要根据传入的参数变化时。
- 性能:
- 直接调用函数作为第二个参数:如果这个函数执行了一些计算密集或副作用的操作,那么在每次组件渲染时都会执行,可能会导致性能问题。
- 使用
init
函数:由于它只在组件的初始化阶段执行一次,所以对于那些计算密集的初始化操作,使用init
函数可能会更为高效。
总结一下,两者都可以用于初始化状态,如果你的初始化逻辑简单并且没有性能顾虑,可以直接使用一个函数作为useReducer
的第二个参数,但如果你需要基于传入的参数来决定初始化逻辑或者想确保性能最优的做法,那么应该使用init
函数。
高级技巧
中间件
就像Redux中的中间件,我们可以利用dispatch
创建一个中间件方法,支持调用dispatch
之前或之后执行代码。
在这个示例中,通过将原始的dispatch
包裹在另一个函数内部,中间件为我们提供了一个在真正的状态更新前后注入自定义逻辑的机会。
示例中,我们在调用原始的dispatch
之前首先检查了action
的类型。实际上,你可以在这里添加任何你想要的逻辑,例如日志记录、错误处理、请求API
等。在dispatch
调用之后,依然可以添加额外的逻辑。
与useContext
一起使用
结合useContext
和useReducer
可以创建简单的全局状态管理系统。
我们就以此来尝试创建一个完整的主题切换系统:
首先,定义状态、reducer 和 context:
接下来,创建一个Provider组件:
在子组件中,你可以轻松切换和读取主题:
这部分代码只是示例说明,完整的使用逻辑和 TypeScript 实现的源码已经发布到我的Github:useReducer-useContext实现主题切换
useReducer
与Redux:主要差异
虽然useReducer
和Redux都采用了action和reducer的模式来处理状态,但它们在实现和使用上有几个主要的区别:
- 范围:
useReducer
通常在组件或小型应用中使用,而Redux被设计为大型应用的全局状态管理工具。 - 中间件和扩展:Redux支持中间件,这允许开发者插入自定义逻辑,例如日志、异步操作等。而
useReducer
本身不直接支持,但我们可以模拟中间件的效果。 - 复杂性:对于简单的状态管理,
useReducer
通常更简单和直接。但当涉及到复杂的状态逻辑和中间件时,Redux可能更具优势。
结语
useReducer
作为 React 的一部分,它比useState
强大,又比 Redux 轻量,尤其适合中小型应用或组件级状态管理。本文把useReducer
的用法和注意项完整的讲解了一遍,吃透其中的知识点就能保证你对useReducer
有足够的了解了。
专栏资源
专栏博客地址:精读React Hooks
专栏演示站:React Hooks Demos
专栏源码仓库:👉Github - Source Code
交个朋友:👉加入「独立全栈交流群」
专栏文章列表:
精读React hooks(一):useState 的几个基础用法和进阶技巧
精读React hooks(二):React状态管理的强大工具——useReducer
精读React hooks(三):useContext从基础应用到性能优化
精读React hooks(五):useEffect使用细节知多少?
精读React hooks(六):useLayoutEffect解决了什么问题?
精读React hooks(七):用useMemo来减少性能开销
精读React hooks(八):我们为什么需要useCallback
精读React hooks(九):使用useTransition进行非阻塞渲染
精读React hooks(十):使用useDeferredValue延迟状态更新
精读React hooks(十一):useInsertionEffect——CSS-in-JS样式注入新方式
精读React hooks(十二):使用useImperativeHandle能获得什么能力
精读React hooks(十三):使用useSyncExternalStore获取实时数据
精读React hooks(十四):总有一天你会需要useId为你生成唯一id