说说 react hooks 做状态管理这件事-hooks+Context 篇
一些废话
想写这篇文章已经有段日子了,探索和实践都在几个月前,不过一直拖着没把它写下来,虽然要介绍的东西本身不难理解,但由于我很久没动笔了所以写的时候有点忐忑,尽量把它写的浅显易懂吧。
背景
react hooks 在过去的一年比较火,给沉淀已久的前端圈带来了不少谈资,官方文档中主要提到了 react hooks 的两个优点:
- 在多个组件中复用逻辑
- 方便书写复杂组件
它并没有标榜能解决 react 开发中的全局状态管理的问题。
以往我们想要在全局应用中同步状态,要么使用 redux/mobx 等数据管理库,要么借用 react 16 推出的 Context API,这几种方式各自有适合的场景,社区里的最佳实践也很多。
一个限制 react hooks 跨组件共享状态的特性是:hooks 只为使用它的组件负责,它只是提供了一些方式去勾入 Class 组件那些生命周期,当然不可能去影响别的组件。
然而写了这篇文章,就不能简单得得出 react hooks 不能用作状态管理这个结论。
事实上这一年中,基于 hooks 的状态管理库层出不穷,甚至有的库标榜自己是【下一代数据管理框架】。不过这篇文章不是去分析这些库的源码,我们一起寻找几种绕过 hooks 的这个限制(特性)的方式,从而实现用 react hooks 作状态管理,当然,我们探讨的前提是应用全部使用【函数组件】开发。
方式一:借助 props
具体做法:在父组件中,将 useReducer 返回的 dispatch 与 state 一并传入子组件,通过在子组件中触发父组件的更新,从而更新其他的子组件。
这非常类似最原始的在 class 组件中使用 callback 的方式,适合组件层级较浅的应用,假如层级较深,状态的传入会变得繁琐以及难以更改和维护,流程示意图:
至于为什么使用 useReducer 而不是将所有状态都用 useState 创建和更新,这一点文档上有说明:
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化, 因为你可以向子组件传递 dispatch 而不是回调函数 。
方式二:借助 Context API
方式一的缺点是在组件层级较深的情况下使用起来比较繁琐,所以可以借助 Context API 避免数据层层传递,Context 上有两个特殊的组件: Provider 和 Consumer 只要 Context 中的值发生改变,就会触发被 Consumer 组件包裹的组件更新,
数据流与方式一相似,不同的地方在于不再使用 props 传入,在子组件中使用 useContext 获取 state 和 dispatch,同时,应用要被 Provider 组件包裹住:
请看这个 在线示例 ,实现这样的一个数据管理库,一般要重写 Provider 组件,把它当成应用的父组件,这都是为了迎合 hooks 的那个特性即:state 更新只影响组件本身,而 Context 只起到一个传递数据和 action 的作用。
相关的库: constate 、 unstate-next 。
方式三:方式二增强版
方式二看起来使用上要方便许多,但依然存在一个缺点,那就是因为使用了 Context,而且每次子组件触发 dispatch 的时候,都会去修改 Context 上的 value 的值,这将导致所有被 Provider 包裹的组件都会重新 render,这一点官方文档上也有强调:
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
有没有什么办法可以优化一下:使得不该更新的组件不 rerender 呢?既然改变了 value 就会触发子组件更新,那么如果 value 是一个固定的值呢?
请看下面的例子:
在这个示例代码中: codesandbox 我们定义了一个 Provider 组件,但是它的 value 是一个通过 useRef 创建的值。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
这使得即便在 Provider 组件中触发了 forceUpdate
子组件依然没有更新,基于这个原理,我们可以修改一下方式二中的使用方式:
请看 这个例子 ,在这个示例中,触发 increase 和 decrease 按钮都不会触发组件更新,只有触发 forceUpdate 才会更新组件,因为触发 increase 和 decrease 的时候,只有 Provider 这个组件重新 rerender 了,但是因为此时 value 值没变,所以子组件不会 rerender。
总不能每次改变应用状态的时候,都要调用一次 forceupdate 吧,再对上面的示例做一次优化,请看这个 优化版本 ,在这个版本中,我们实现一个简单的发布订阅库,在 useStore
方法中订阅子组件的 forceUpdate 到 subscribers
中 ,在 Provider 组件 render 的时候,调用 notify
方法依次调用 subscribers
中的所有 foreUpdate
,以此来触发子组件 rerender。
实际上这是社区中 reto 这个库早期的实现。
结语
以上是用 react hooks + context 实现应用状态管理的三种方式,当然社区中并不是只有这几种实现方式,更多的留给读者自己去探索,下一篇我们将介绍不借助 Context 实现状态管理的实现。