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)

场景 dva zustand mobx recoil/jotai hox umijs@4.x
状态共享 支持 支持 支持 支持 支持 支持
业务逻辑共享 effects function 不提供 不提供 hooks 自由实现 hooks 自由实现
状态拆分/模块化 namespace 独立 store 支持任意属性、对象 独立 atom 独立 store 独立 store
共享状态依赖 不支持 自定义 useStore 实现,或 subscribe,不支持 create 时的 getter computed selector hooks 直接依赖即可 需要通过 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 是同一个开源团队的作品,而且他们的名字分别是 德语、芬兰语、日语 中的“状态”。所以,不选择某个工具,不是说这些工具不好,他们之间并没有孰优孰劣,只是面向风格不一样罢了。
相关文章
|
14天前
|
前端开发 JavaScript API
react 常用的状态管理
【8月更文挑战第29天】react 常用的状态管理
13 1
|
22天前
|
前端开发 JavaScript 算法
深入剖析React状态管理的优势与局限
【8月更文挑战第20天】
62 3
|
22天前
|
存储 前端开发 JavaScript
|
30天前
|
前端开发
React——开发调式工具安装【五】
React——开发调式工具安装【五】
21 0
React——开发调式工具安装【五】
|
11天前
|
容器 Kubernetes Docker
云原生JSF:在Kubernetes的星辰大海中,让JSF应用乘风破浪!
【8月更文挑战第31天】在本指南中,您将学会如何在Kubernetes上部署JavaServer Faces (JSF)应用,享受容器化带来的灵活性与可扩展性。文章详细介绍了从构建Docker镜像到配置Kubernetes部署全流程,涵盖Dockerfile编写、Kubernetes资源配置及应用验证。通过这些步骤,您的JSF应用将充分利用Kubernetes的优势,实现自动化管理和高效运行,开启Java Web开发的新篇章。
22 0
|
11天前
|
前端开发
【实战指南】React Hooks 详解超厉害!六个步骤带你提升 React 应用状态管理,快来探索!
【8月更文挑战第31天】React Hooks 是 React 16.8 推出的新特性,允许在函数组件中使用状态及其它功能而无需转换为类组件。通过以下六个步骤可有效提升 React 应用的状态管理:1)使用 `useState` Hook 添加状态;2)利用 `useEffect` Hook 执行副作用操作;3)在一个组件中结合多个 `useState` 管理不同状态;4)创建自定义 Hook 封装可重用逻辑;5)借助 `useContext` 访问上下文以简化数据传递;6)合理运用依赖项数组优化性能。React Hooks 为函数组件带来了更简洁的状态管理和副作用处理方式。
16 0
|
11天前
|
Web App开发 监控 前端开发
React 性能监测工具大揭秘!Chrome DevTools 高级用法来袭,让你的 React 应用性能飙升!
【8月更文挑战第31天】在前端开发中,React 框架虽简化了高效、交互性强的用户界面构建,但应用复杂性增加亦可能引发性能问题。此时,Chrome DevTools 凭其性能面板成为了优化应用性能的重要工具,能帮助开发者记录与分析加载时间、渲染及脚本执行等性能指标,定位并解决性能瓶颈。同时,其 React 开发者扩展工具允许实时监控组件状态变化,进一步提升性能。结合运用这些功能,将有助于打造流畅的用户体验。
19 0
|
11天前
|
存储 JavaScript 前端开发
探索React状态管理:Redux的严格与功能、MobX的简洁与直观、Context API的原生与易用——详细对比及应用案例分析
【8月更文挑战第31天】在React开发中,状态管理对于构建大型应用至关重要。本文将探讨三种主流状态管理方案:Redux、MobX和Context API。Redux采用单一存储模型,提供预测性状态更新;MobX利用装饰器语法,使状态修改更直观;Context API则允许跨组件状态共享,无需第三方库。每种方案各具特色,适用于不同场景,选择合适的工具能让React应用更加高效有序。
23 0
|
22天前
|
存储 JavaScript 前端开发
"探索Redux的Vuex化:如何在React世界中享受Vue状态管理的优雅与强大"
【8月更文挑战第21天】在现代前端开发中,状态管理至关重要。Vuex作为Vue.js的状态管理库,通过集中式存储和严格规则确保状态变更的追踪。Redux则以其在React生态中的可预测性和灵活性著称。两者都强调单一数据源、状态只读及使用纯函数变更状态。尽管API设计不同,理解Redux的核心概念——单一数据源(`store`)、状态只读与纯函数变更(`reducers`),并参考Vuex的`state`、`mutations`等,能帮助开发者快速掌握Redux,高效管理应用状态。
13 0
|
22天前
|
JavaScript 前端开发 安全