写在前面

这将会是一篇系列博文,在本文中,我会谈一谈我对现今社区中流行的状态管理方案的看法,在后续文章中,我会介绍如何实现一个状态管理库。

目录

为什么我们需要状态管理库

react 自己也提供了状态管理的解决方案,那就是 useContext + useReducer。很多人都说,使用这二者作为状态管理已经足够了。简单场景下确实如此,我在实现了一个状态管理库并将其应用在我的项目中之前,也一直是使用这个方案来管理项目中的各种状态的。context 是个很好的 api,一直以来也是我使用最多的 react api 之一。使用 context 可以做到只要声明一次数据,就可以在其下层任意位置随意使用,大大降低了组件间通信的成本,不用为了同步状态而陷入 props 的迷宫之中,这有利于组件的拆分,使代码更灵活。

但 context 也有一些老生常谈的缺点。

  1. context 可能会导致组件不必要的重绘。比如传入 context 的是一个对象,对象的某一个属性变化,所有订阅了 context 的子组件都会重绘,哪怕有的组件并没有用到这个属性;
  2. context 需要配合 provider 使用,这将数据与 UI 耦合在了一起。reducer 也并不能独立于组件发挥作用,你必须要声明一个组件,用来引用 reducer,并将其值传递给 provider;
  3. context 必须将状态提升到顶层。这可能会给组件划分带来影响,开发者必须迁就 context 的逻辑。而且如果以后你发现需要在 Provider 外使用 context 的值,还要对组件进行重构,不利于维护。

我觉得这就是状态管理库存在的意义之一。状态管理库的状态独立于 react 而存在,不存在值与 UI 耦合的问题,也不存在状态提升的问题。状态管理库旁观着 react,当 react 中任何地方需要使用其值,直接拿去用便是。有的状态管理库也会提供一个 Provider,但它们的文档中一定写了类似"请将 Provider声明于最接近组件树顶层的位置"这样的话,这样它们提供的状态天生就是全局可用的。

原子状态

近些年来,以 recoil 和 jotai 为首的原子状态管理库开始崭露头角,它们这种自底向上的状态管理方式,与 redux 这种自顶向下的状态管理方式截然相反。每一个状态都是独立的,这种方案有一个天然的优势,那就是组件更新粒度更细。它们可以不用付出额外的努力,就能做到只让订阅了某个原子状态的组件更新,订阅了其它原子状态的组件则不需要更新。而 redux 想做到这点,需要依靠 selector,从总的 store 中过滤出需要的字段。

其实这些原子状态底层依然是存储在一个 store 中的,但是在表现形式上,各原子状态相互独立,彼此没有关联,想把它们联系到一起,则是通过类似 vue 中 computed 的方式。

我个人很喜欢原子状态这种表达方式,所以我为我的项目设计的状态管理库也是原子化的。

远程状态

没错,现在一些前沿的思想已经把服务器状态也考虑为前端的一种特殊的状态了,其中的代表便是 react-query

一些比较早的网络请求库,仅仅起到了发送网络请求的作用,比如 ahooks 的 useRequest。每一次、每一个地方发出的网络请求,彼此之间都是孤立的,没有任何关联。而且这些库提供的 api 通常是命令式的,为你提供一个 onFetch 之类的方法,由你在 useEffect 中或其它什么地方手动触发网络请求。

而 react-query 更进一步。它将项目中所有的网络请求考虑为一个整体,维护在自己的 client 中,只要传入的 queryKey 相同,哪怕两个发起网络请求的组件相隔半个组件树那么远,react-query 也将其认为是同一个状态。并且 react-query 更新远程状态的方式是声明式的,只要 queryKey 变化,react-query 会认为出现了一条新的 query,从而自动发起请求,开发者不再需要手动触发请求更新数据。react-query 自己解决了远程状态的存储、更新和过期等问题,在开发者看来,从 react-query 中拿到的状态,除了是异步的,会有 loading 效果之外(甚至这个 loading 效果也只在一条 query 第一次被请求时才存在),似乎和一个普通的从 useContext 中拿到的状态没有什么区别。react-query 是一个杰出的网络请求和状态管理库,有没有引入 react-query 的项目之间,代码的差异是非常巨大的,这不仅是网络请求的发起方式的区别,也是远程状态管理方式的区别。

其实仔细想来,前端所呈现的状态,绝大部分都是远程状态,用户的信息、网站的内容,这些都不存储在前端。可以说,在使用 react-query 之后,90% 的状态都可以从 react-query 中获取了。那剩下的 10% 的状态,还需要用另一个状态管理库来维护吗?

我的答案是,需要。

最后一步

我在之前已经提到了状态管理库存在的意义——useContext 存在着一些缺陷,而状态管理库可以避免这些问题。但在已经由 react-query 接管了绝大部分状态的情况下,引入一个为了管理所有状态而存在的状态管理库似乎显得过重了。

现今市面上的状态管理库,几乎全都是为了自己可以接管所有状态而存在的。它们支持异步方法,可以在其中发送网络请求,并将网络请求的结果直接存入 store 中。但问题在于,react-query 已经如此好用,我为什么要在这些库中发起网络请求,还要再自己处理一遍竞态、突变、数据过期等问题呢?而且即使我真的把这些问题都处理一遍,也几乎不可能做得比 react-query 更好了。

再加上我在实际工作中,也确实遇到了一些依靠现今市面上的状态管理库提供的 api 难以解决的问题,所以我就萌生了自己开发一个状态管理库的想法。

我将它定位为维护除 react-query 状态之外那剩余的一点状态的工具,是补全 react 状态管理的最后一步,它的意义在于成为更好的 context。它非常轻量,因为不需要提供网络请求的支持,只提供一个外部数据源,以及一系列更新数据源的方法。同时它可以按照我的生产需求方便地扩展 API。同时它需要能够很好地与 react-query 配合,比如为将 react-query 请求到的结果存储为表单的默认值这种场景提供便利。

总结

这篇文章介绍了我认为的状态管理库存在的意义,比较了原子状态和统一 store 这两种方案的区别,提出了远程状态的概念,以及我理想中的状态管理库是什么样子。

下一篇文章 中我会介绍这个我理想中的状态管理库的 api 设计。

原创作品自问世起即受到版权保护,欢迎前往 github 交流,请勿抄袭❤