说说 react hooks 做状态管理这件事-hooks+Context 篇

一些废话

想写这篇文章已经有段日子了,探索和实践都在几个月前,不过一直拖着没把它写下来,虽然要介绍的东西本身不难理解,但由于我很久没动笔了所以写的时候有点忐忑,尽量把它写的浅显易懂吧。

背景

react hooks 在过去的一年比较火,给沉淀已久的前端圈带来了不少谈资,官方文档中主要提到了 react hooks 的两个优点:

  1. 在多个组件中复用逻辑
  2. 方便书写复杂组件

它并没有标榜能解决 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 上有两个特殊的组件: ProviderConsumer 只要 Context 中的值发生改变,就会触发被 Consumer 组件包裹的组件更新,

数据流与方式一相似,不同的地方在于不再使用 props 传入,在子组件中使用 useContext 获取 state 和 dispatch,同时,应用要被 Provider 组件包裹住:

请看这个 在线示例 ,实现这样的一个数据管理库,一般要重写 Provider 组件,把它当成应用的父组件,这都是为了迎合 hooks 的那个特性即:state 更新只影响组件本身,而 Context 只起到一个传递数据和 action 的作用。

相关的库: constateunstate-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 实现状态管理的实现。