开发者社区> 广州-王康宁> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

React 状态管理工具:我是这样选择的

简介: React 状态管理工具五花八门,dva、mobx、recoil、zustand。换做是你,你会怎么选呢?选择一个合适的状态管理工具,对项目研发是至关重要的,来看看我的选择方案吧
+关注继续查看

前言

我们的前端团队在一直深度使用 React ,从最早的 CRA ,到后来切换到 umijs ,从 1.x、2.x、3.x 再到现在的 4.x,其中有一点不变的,就是我们一直在使用基于 react-redux 思想的 dva 作为状态管理工具。

Pasted image 20221010153251.png

在状态共享这方面,不像 VuexReact 的官方并没有强力推荐某种封装方案,所以 React 的状态管理工具五花八门,百花齐放。其中就有:

  • 做什么都要 dispatchredux 流派。包括:react-reduxdva、新星代表 zustand
  • 响应式流派 mobx。以及新星代表 valtio ,以及一个很有特点的库 resso
  • 原子状态流派。来自 facebook 开源的 recoil ,以及新星代表 jotai
  • 完全体 hooks 流派。hoxretoumijs@4 内置数据流,包括 Vue 官方推荐的新状态管理工具 pinia 也是这个流派。

Pasted image 20221010144421.png

更为重要的一点是,传统的 MVC 模式,建议我们将视图层的逻辑层分离。而在 dva 中,页面即视图,effects 则被我们用于业务逻辑的编写。在这种思想的影响下,不论是简单还是复杂的页面,我们都习惯去创建一个 dva-model,再加上 dispatch 都不是强依赖关系,久而久之, model 越来越臃肿,关系越来越难找,吐槽声越来越大。

随着技术不断发展,我们终归是要摆脱繁琐的 dva,寻找一个新的状态管理工具,来减少我们这一块的代码量,保护我们的秀发。

所以经过了一系列的试点,我也来介绍一下各流派的优缺点和我个人的倾向。

我们需要什么样的状态管理工具

可能不需要?

我们阅读一些状态管理工具的文档时候,可能就会先被这样一篇文章甩在脸上:《你可能不需要状态管理工具》

Pasted image 20221010154205.png

是的,不管是用起来繁琐的 dva 还是更为简洁的 recoil ,我们都不应该滥用状态管理工具。滥用只会给我们后面的维护和重构带来麻烦。

什么时候需要?

状态管理工具的作用,就是状态的共享,当共享状态发生变化,所有使用方都会触发重新渲染。所以,当然是状态需要被多方共享的时候,才需要使用状态管理工具了。比如:

  • 当前登录的用户信息,姓名,角色,所属组织等
  • 静态数据字典的缓存
  • 需要 keep-alive 的数据(不一定用)
  • 页面功能复杂,模块化后,模块之间仍需要共享的数据
请不要在【基础组件】中使用共享的状态,【基础组件】应该保持自身的独立性,做到高内聚

对状态管理工具的要求?

随着项目经验的积累,我总结出了状态管理工具应该满足的几个特性:

  1. 共享状态(基础),能够满足上面列举的几个场景;
  2. 共享业务逻辑,比如修改个人密码后需要退出并跳转至登录页(在菜单栏和个人中心都要调用,相同的逻辑不应该写多次);
  3. 共享状态模块化,即按不同业务逻辑,分开不同的文件创建共享状态。
  4. 再复杂一些的,涉及到共享状态之间的依赖,比如当我修改当前登录人的角色之后(比如从”项目经理“切换至”系统管理员“),记录菜单权限的状态也需要更新。
  5. 使用时,有清晰的依赖来源(import from)。
  6. 对 TypeScript 支持良好,易编写。

主流状态管理工具都是怎么做的

传统流派 dva

Pasted image 20221010162917.png

Pasted image 20221010163018.png

对比 dvaVuex 不能说是非常相似,只能说是一模一样了。

  • dvastateVuexstate,用于存放需要共享的状态。
  • dvareducersVuexmutations,用于编写修改共享状态的方法。
  • dvaeffectsVuexactions,用于编写存在副作用的方法,比如处理异步业务逻辑。
  • dva 使用 namespace 属性标记子模块的名称,Vuex 使用 modules 属性,拆分子模块。

优点

  • 在当时没有 hooks API 的环境下,算是一套不错的整合方案,能够满足绝大多数的共享业务场景。
  • 深度整合 redux redux-saga ,便于 redux 用户能够快速切换。

缺点

  • 使用时没有清晰的来源
    dva 使用 hoc connect 的方式,将 store 中的属性注入到组件的 props 上。如下图:以 JS 的角度来看,products 的来源和类型都是非常不清晰的。
    Pasted image 20221009144954.png
  • TypeScript 支持不友好
    没有清晰的依赖关系,类型支持自然也是很差的。
  • 不能满足共享状态之间的依赖
    比如我修改了当前用户角色,需要根据角色权限重新查询可访问的菜单。但我不能在菜单store中监听,只能在用户store主动触发菜单查询,或单独写一个组件,利用 componentDidUpdate 监听 用户store 再发起请求。

zustand

拥有 22K stars 的 zustand 则是非常值得尝试的传统派替代品。

  • 它面向 hooks API,一个 store 就是一个 hooks
    Pasted image 20221009152647.png
  • 它更简洁,直接将 state / reducers / effects 平铺开来,functioneffects / reducers ,其它都是 state
  • import 来源清晰,对 TypeScript 的支持也更友好
  • 提供了 subscribe 接口,可在组件外监听状态变化,以实现状态依赖。

Pasted image 20221010165125.png

基本把上述的缺点都解决了。

响应式流派 mobx

Pasted image 20221010165515.png

mobx 的出现给 redux 带来了很大的冲击。

通过一个装饰器(observer / observable),就能使普通的组件能够监听变量的变化而渲染,完全抛弃掉 state 。不仅如此,mobx 提供了 computed 等一些好东西,使 React 也能使用到 Vue 组件的特性。而从 Vue 转过来学习 React 的同学,都会对 mobx 拍手称赞。

在体验过 mobx 的爽快之后,当时团队中有部分声音,是希望以后抛弃掉 state,转而全面使用 mobx 作为组件状态。是啊,一套方案,解决内部状态和共享状态两个问题,何乐而不为呢。

缺憾

很快,这种 mobx 为王的声音很快又消失了,因为...

  • 响应式 API 和 React 水土不服,React 就是需要 setState 来修改状态,现在你 state.value++ 就改了,是要造反么?新来的同学学完 React 基础,结果和他说,那些用不上,再学学 mobx 响应式吧,新同学是否会心中充满问号?
  • 会有一些隐蔽的坑,比如往 observable 添加属性时,不能直接添加,而要通过 extendObservable ,我们有很多同学踩了这坑。
    Pasted image 20221009155725.png
  • 基础组件如果也使用 mobx 则违反了高内聚的原则,不使用,两边风格又不统一。你见过哪个组件库需要附带一套状态管理工具的?
  • 响应式其实就是基于 Proxy 实现的,我明明希望传递的是一个数组,但拿到的却是一个 Proxy。强迫症实在受不了。

所以,mobx 很优秀,但我实在爱不来。

原子状态流派 recoil

Pasted image 20221010165859.png

体验过 recoil 之后,我能感受到,recoil 是希望你在使用全局状态时,和 useState 的体验完全一致。是的,useRecoilStateuseState 的使用方式几乎是完全一样的,只不过 recoil 的默认值需要使用 atom 包裹一下罢了。于是你的 atom 状态就实现全局共享了。

Pasted image 20221010165947.png

为了解决共享状态依赖的需求,recoil 还很贴心地提供了 selector API,用于实现共享状态的拆分和依赖,你把它当作 useMemo 或者计算属性来看待就可以了。(当然 selector 还有支持写入(set)以及异步处理,但我还没找到必须要用它的场景)

不足

recoil 理念真的很简单,就是以 useState 的习惯实现状态共享。所以在业务逻辑共享这一块,它似乎没有给出很好的方案。但是既然已经是面向 hooks API 了,自定义 hooks 本身就可以实现业务逻辑的复用了。比如下面这段伪代码:

// src/hooks/useChangePassword.js

// 修改密码动作
export function useChangePassword(){
  // 当前用户信息的共享状态
  const [userInfo, setUserInfo] = useRecoilState(userAtom);

  // 修改密码
  const changePassword = async (oldPassword, newPassword) => {
    // 1. 调用修改密码接口
    const result = await post('/api/password', { oldPassword, newPassword });

    if(reuslt.success) {
      // 2. 清空当前用户信息
      setUserInfo(null);
      
      // 3. 跳转至登录页
      history.push('/login');
      
      // 4. 提示信息
      message.warn('已修改密码,请重新登录');
    } else {
      // 操作失败提示
      message.error(result.errMsg);
    }
  }

  return changePassword;
}

这样,在个人中心菜单栏密码过期的几个场景中,我都可以这一段 hooks 实现修改密码后的系列动作,而不是每个地方都调用一次接口。

jotai 几乎是完全对标 recoil 的,我就不赘述了

hooks 完全体 -- hox

Pasted image 20221010170053.png

hox 刚出来不久,我就关注到了,并觉得其思想非常棒。但翻阅了一下源码后发现,它依赖了一个实验性的渲染器 react-reconciler,以至于我不敢将它用于生产环境。直到 react@18.x 带来了一个新的 hooks: useSyncExternalStore ,以及基于它实现的 hox@2.x

我们来看它的介绍:

在 hox 中,任意的自定义 Hook,经过 createStore 包装后,就变成了持久化,可以在组件间进行共享的状态。

我的天,真的太神奇了,你只要用 createStore 包裹你写的某个 hooks ,它里面的状态就变成了可共享的了。

实现原理

举个简单的例子,我写了一个自定义 hooks useMyHook:

export const useMyHook = () => {
  const [value, setValue] = useState(1);

  return [value, setValue];
}

我在 组件 A组件 B 中都使用了它。正常情况下,A 和 B 中的 value 当然是不同的。

但是假如我“偷偷地”将 hooks 放在最外面执行,比如 App 下,然后再用 Context 传递下去:

// App.tsx
export const Context = React.createContext({});

export default function App() {

  // 在最外层执行 hooks
  const myHookResult = useMyHook();

  // 通过 Context 向下传递
  return (
    <Context.Provider value={myHookResult}>
      {children}
    </ContextProvider>
  )
}

我再给你一个封装后的 Hook:

// 在 组件A 和 组件B 中使用这个 hooks
export const useMyHookWrapped = () => {
  // 从外部获取到 useMyHook 的内容
  const myHookResult = useContext(Context);
  return myHookResult;
}

综上,你就会发现,相当于 useMyHook 只被使用了一次,其它需要用到的地方,都是使用 useContext 获取的。那么自然就实现了状态共享了。而这些,都是 createStore 实现的。

Pasted image 20221010171828.png

并且,和状态相关的业务逻辑,也写在了同一个 hooks,还可以不受限制地获得完整的 hooks 体验,使用第三方 hooks 库。最令人惊叹的是,由于都是 hooks API,你可以先在组件中编写业务逻辑,当发现逻辑需要共享时,直接复制抽离出去;或者是当你需要迁移部分功能到另外的项目,不需要共享了,只需要去掉 createStore ,它就又变成了普通 hooks 了。

umijs@4.x 数据流方案

umijs@4.x 正式推出后,我注意到它内置了一套和 hox 一模一样的数据流方案。我大概翻了一下,虽然它不是直接引用的 hox ,但内部实现逻辑如出一辙。

它的特点是,采用约定式目录结构,不用专门写 createStore ,而是自动帮我们引入了所有 model 目录下的 hooks,并注册。在页面中则是通过统一的 useModel,通过其自动生成的 namespace 引用,比如 useModel('product')。但这也导致了依赖不明确的问题,umi4 还特地通过编写插件的方式解决跳转问题。

但也正是它的约定式,产生了一些让我觉得膈应的地方:

  1. useModel('product') 必须要通过装插件才能点击跳转。
  2. 必须要在 umijs@4.x 体系下才能使用,无法快速复制迁移到其它的框架下使用。
  3. 插件偷偷帮你实现了 createStore,乍一看和普通的 hooks 完全一样,其实已经持久化了。对新人学习很不友好。(你写个 createStore 他还知道有不一样的地方,回去查。umijs@4.x 没看到文档根本不知道有这回事)

我的选择

对几个工具的主观评价(满分5)

场景dvazustandmobxrecoil/jotaihoxumijs@4.x
状态共享支持支持支持支持支持支持
业务逻辑共享effectsfunction不提供不提供hooks 自由实现hooks 自由实现
状态拆分/模块化namespace独立 store支持任意属性、对象独立 atom独立 store独立 store
共享状态依赖不支持自定义 useStore 实现,或 subscribe,不支持 create 时的 gettercomputedselectorhooks 直接依赖即可需要通过 useModel 依赖
使用时的依赖清晰不支持支持支持支持支持不支持
对 TypeScript 友好⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐<br/>(5分给atom,2分给selector)⭐⭐⭐⭐⭐⭐⭐⭐⭐
和 React 亲和程度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

综上,你可以发现,hox 的方案算是所有工具中最好的,能够满足所有的场景,并获得不错的评分。我也会在今后的工作中更深入地使用 hox 方案,发掘其是否还有更优秀的用法或隐藏的问题。

最后也欢迎各位一同讨论你们在状态管理工具上的取舍。

PS:为什么不选

zustand

zustand 很优秀,但使用它仍然会让我联想到在 effects 中使用 dispatch 更新状态的痛苦时光。是啊,在 zustand 你还是要使用 set 来修改状态。虽然它也提供了额外的 setState 方法让你直接更新状态,但是风格和 useState 差得远啊,让我觉得不伦不类。

mobx / valtio

这两个很明显了,响应式的风格,跟 React 对着干,我当然是不选的。

recoil / jotai

我原本是比较倾向于 recoil 的,奈何 selector 的用法有一定的上手成本,让新手学完 useMemo 就能运用上不好么。

细心点你会发现,zustand / valtio / jotai 是同一个开源团队的作品,而且他们的名字分别是 德语、芬兰语、日语 中的“状态”。所以,不选择某个工具,不是说这些工具不好,他们之间并没有孰优孰劣,只是面向风格不一样罢了。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
状态提升(精读React官方文档—10)
状态提升(精读React官方文档—10)
44 0
React Native webView postMessage报错
报错如下: Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined.
2207 0
Airbnb弃用之后,我们还应该用React Native吗?
近日,Airbnb 发表了一组由 5 篇博文组成的系列文章(https://medium.com/airbnb-engineering/react-native-at-airbnb-f95aa460be1c),他们在文章中宣布停止使用 React Native,并将其从代码库中移除,转而使用 Swift/Objective-C/Java/Kotlin。
2740 0
react native实现原理解析(从源码入手,nice)
http://www.cocoachina.com/programmer/20170505/19189.html
1191 0
React Native系列(6) - 编译安卓私有React-Native代码
为何要自己编译React Native安卓私有代码 我们在开发中遇到一个HTTP2的问题,React Native安卓客户端在和HTTP2支持的服务器通讯的过程中会有crash,见 React-Native HTTP2 issue How to build private build 由于时间紧急,发布期限已经拖了好久了,没法等待官方解决方案,只能在本地做修复然后发布。
1746 0
React Native中添加Base64支持
最近极客人在使用React Native制作的自己的博客客户端,客户端在调用Wordpress Rest API时有些操作需要使用HTTP认证,而HTTP认证中主要就是在HTTP请求中的头部加入 "Authorization"字段,Authoriza...
1031 0
Windows下React Native开发01 -- Android开发环境搭建
1.安装jdk 推荐将JDK的bin目录加入系统PATH环境变量(自己百度下怎么配置)。   2.安装SDK 直接安装 Android Studio  推荐从AndroidDevTools下载。(也可以直接安装 android sdk,这里是直接安装的android tools)   1.
869 0
文章
问答
文章排行榜
最热
最新
相关电子书
更多
利用编译将 Vue 组件转成 React 组件
立即下载
搭建React Native生态
立即下载
React在大型后台管理项目中的工程实践
立即下载