我对 React 状态管理库无休止的争论的看法(setState/useState vs Redux vs Mobx)

2025-06-10

我对 React 状态管理库无休止的争论的看法(setState/useState vs Redux vs Mobx)

与我之前的文章不同,这篇文章将更多地基于个人观点。所以,亲爱的读者,请谨慎看待本文的一切——这只是我对 React 状态管理问题的感受、想法和观点。

你为什么要听我的?

我曾在 React 的商业项目中使用过三种最流行的状态管理方法:

  • 只需使用 React 内置状态机制,
  • 使用 Redux,
  • 使用 Mobx。

因此在本文中我将比较这三个选项。

我的目标是为你提供对每种方法的平衡意见,但更重要的是,给出一个(肯定有争议的)意见,说明为什么状态管理成为 React 应用程序中如此重要的问题,导致人们就该主题编写了无数的库、文章和会议演讲,而这些问题可能早就应该解决了。

让我们开始吧!

国家的起源

我刚开始学习前端开发的时候,没人提过“状态管理”。也没人真正关心状态。

在我开发的第一个商业应用程序中,使用不朽的 jQuery 库编写,人们只是将状态存储在一些随机的位置(例如某些 HTML 元素的“data-*”属性),或者根本没有将其存储在任何地方。

在第二种情况下,读取状态意味着简单地检查 DOM 中当前渲染的内容。那个对话框窗口打开了吗?没有布尔值告诉我们这一点,所以我们就直接检查树中是否有一个带有特殊 class 或 id 的 DOM 元素吧!

当然,这种方法会导致代码库极其混乱且充满错误,因此 React 的方法(将应用程序的状态与视图明确分离)对我们来说是一个巨大的顿悟,也是应用程序状态的概念永远根植于我们脑海中的时刻。

React 状态机制(经典和现代)

由于 React 将状态作为独立实体的概念引入,它还引入了一些简单的工具来管理该状态。

之前它只是一个setState允许修改存储在指定组件中的状态的方法。现在我们也新增了一个useState钩子,虽然表面上有一些区别,但最终目的是一样的——基于每个组件定义和修改状态。

现在,最后这一点至关重要。在 React 中,每个状态都是在组件“内部”定义的。因此,不仅假设一个组件FirstComponent拥有独立于 的状态SecondComponent,就连 的每个实例也FirstComponent都有自己的状态实例。这意味着(至少在开箱即用的情况下)React 组件之间不会共享状态。每个组件都有自己创建并管理的状态实例,仅此而已!

但事实证明,我们经常希望在网站的不同位置(因此在不同的组件中)显示相同的状态。

例如,应用程序顶部 Facebook 标题中的新消息数量应始终等于 Messenger 窗口本身底部的未读消息数量。

拥有一个共享状态(消息列表,其中一些被标记为“未读”)将使这变得简单,确保两个组件始终显示相同的信息。

Messenger组件会简单地显示列表中的消息,并用粗体标记未读消息。同时,Header组件会计算列表中标记为未读的消息数量,并将该数字显示给用户。

或者,如果该状态有两个单独的副本(一个在Header组件中,另一个在Messenger组件中),可能会导致这些状态不同步。例如,用户可能会看到 中有两条未读消息Header,但在 中却找不到任何未读消息Messenger。这肯定会很烦人。

那么我们如何才能仅使用 React 而不使用任何其他库来实现状态共享呢?

共享状态的典型方法是将其存储在单个组件中,位于组件树的较高位置。然后,您可以简单地将此状态作为 props 向下传递。因此,您可以通过 props 将相同的状态传递给两个独立的组件……然后……轰!这两个组件现在共享该状态了。

一开始这种方法效果很好。但是,如果你以这种方式编写应用程序(并且它们变得足够复杂),你很快就会注意到,随着时间的推移,很多状态会“冒泡”。

随着越来越多的组件需要访问相同的状态,您会将该状态在组件树中放置得越来越高,直到最终到达最顶层的组件。

所以最终你会得到一个巨大的“容器”组件,它基本上存储了你所有的状态。它有几十种方法来操作这些状态,并通过几十个 props 将状态传递给几十个组件。

这很快就会变得难以管理。而且,实际上没有简洁易行的方法可以将这些代码拆分成更小的片段。最终你会得到一个庞大的组件文件,其中通常包含超过一千行代码。

你最终会陷入和之前用 React 将状态与视图分离时类似的混乱局面。哎呀……

Redux 来救援

Redux 的发明原因与我们上面描述的略有不同。事实上,它纯粹是一个演示工具,旨在展示“时间旅行”在 React 应用开发中的潜力。

事实证明,如果你将所有状态都放在一个地方(称为“store”),并且始终一次性更新所有状态(使用“reducer”函数),那么你基本上就拥有了“时间旅行”的能力。由于你可以序列化存储在 store 中的状态,并在每次更新后保存,因此你可以保留所有过去状态的历史记录。

然后,您只需按照命令返回到任何过去的状态,只需将它们重新加载到存储区即可。现在,您正在进行时间旅行——在应用程序的历史记录中回到过去。

时间旅行最初被认为是一种有助于开发和调试 React 应用程序的方法。这个想法听起来很棒,很快就吸引了众多用户的关注。

但事实证明,这种功能并不像人们最初想象的那么有用。事实上,我认为目前大多数现有的 Redux 应用都没有显著地利用时间旅行,即使是出于调试目的。这实在太过繁琐,毫无价值(尽管我仍然坚信console.log基于 的调试)。

然而,我相信 Redux 的一个特性使得它从一开始就成为编写复杂 React 应用程序的主要工具。

正如我们所说,Redux 中的状态不再基于每个组件创建。相反,它被存储在一个中央内存数据库中,正如我们提到的,这个数据库被称为 store。

因此,任何组件都可能访问此状态,而无需通过 props 向下传递,这实在太麻烦了。在 Redux 中,任何组件都可以直接访问 store,只需使用一个特殊的实用函数即可。

这意味着您保存在商店中的任何数据都可以在应用程序的任何位置显示,而且只需很少的努力。

由于多个组件可以同时访问状态而不会出现任何问题,因此状态共享也不再是问题。

现在,我们的 Facebook 网站可以在任何我们想要的地方显示未读消息的数量,只要我们将消息列表保存在商店中。

将所有状态存储在一个地方听起来可能有点类似于我们将所有状态保存在单个组件中的方式。但事实证明,由于 Redux 存储的更新是由 Reducer 函数完成的,而这些函数非常易于组合,因此将我们的 Redux 代码库拆分为多个文件(按领域或职责划分)也比管理一个庞大的“容器”组件要容易得多。

所以 Redux 听起来确实像是我们之前描述的所有问题的解决方案。React 的状态管理似乎已经解决了,我们现在可以继续讨论更有趣的问题了。

然而,正如生活中一样,事实并非如此简单。

Redux 还有两个部分我们尚未描述。

虽然组件可以直接读取 Redux Store,但它们无法直接更新Store。它们必须使用“actions”来请求 Store 自行更新。

最重要的是,Redux 被认为是一种同步机制,因此为了执行任何异步任务(例如 HTTP 请求,这对于 Web 应用程序来说并不是一个疯狂的要求),您需要使用授予 Redux 操作异步功能的“中间件”。

所有这些部分 - 存储、reducer、动作、中间件(以及一大堆额外的样板)使得 Redux 代码非常冗长。

在 Redux 中,更改一个简单的功能通常会导致修改多个文件。对于新手来说,追踪典型的 Redux 应用程序中正在发生的事情极其困难。起初看似简单的事情——将所有状态存储在一个地方——很快就变成了极其复杂的架构,人们需要数周时间才能习惯。

人们显然感受到了这一点。在 Redux 成功之后,各种状态管理库大量涌入。

大多数库都有一个共同点——它们试图做与 Redux 完全相同的事情,但样板更少。

Mobx 成为了最受欢迎的之一。

Mobx 的魔力

与 Redux 对函数式编程的关注相反,Mobx 决定毫无保留地接受老派的面向对象编程 (OOP) 哲学。

它保留了 Redux 的 store 概念,但将其简化为一个带有一些属性的类。它保留了 Redux 的 actions 概念,但将其简化为方法。

不再有 Reducer,因为你可以像在常规类实例中一样更新对象属性。不再有中间件,因为 Mobx 中的方法既可以是同步的,也可以是异步的,这使得机制更加灵活。

有趣的是,理念保持不变,但实现却大相径庭。最终,它所构建的框架——至少乍一看——似乎比 Redux 更轻量。

最重要的是,Mobx 使用的语言对普通软件开发人员来说更加熟悉。几十年来,面向对象编程一直是程序员教育的一部分,因此,对于绝大多数刚接触 React 的程序员来说,通过类、对象、方法和属性来管理状态更加熟悉。

再一次,我们似乎已经解决了我们的问题——我们现在有一个状态管理库,它保留了 Redux 的思想和优点,同时对新手来说不那么冗长和陌生。

那么问题出在哪里呢?事实证明,Redux 明显复杂且冗长,而 Mobx 则隐藏了它的复杂性,假装是大多数开发人员熟悉的编程模型。

事实证明,Mobx 与 Rx.js 甚至 Excel 的共同点远多于传统的面向对象编程 (OOP)。Mobx看起来像面向对象编程,但实际上其核心机制基于截然不同的理念,甚至比 Redux 所倡导的函数式编程对普通程序员来说更加陌生。

Mobx 不是一个 OOP 库。它是一个响应式编程库,隐藏在类、对象和方法的语法之下。

问题是,当你使用 Mobx 对象并修改其属性时,Mobx 必须以某种方式通知 React 状态已发生更改。为了实现这一点,Mobx 采用了一种受响应式编程概念启发的机制。当属性发生更改时,Mobx 会“通知”所有使用该属性的组件,作为响应,这些组件现在可以重新渲染。

到目前为止这很简单并且运行完美,这也是 Mobx 可以用如此少的样板实现如此多 Redux 功能的原因之一。

但 Mobx 的反应能力并不止于此。

某些状态值依赖于其他状态值。例如,未读消息数量直接取决于消息列表。当列表中出现新消息时,未读消息数量应该相应增加。

因此在 Mobx 中,当属性发生变化时,库机制不仅会通知显示该属性的 React 组件,还会通知依赖于该属性的其他属性。

它的工作原理与 Excel 类似,在您更改一个单元格的值后,依赖于该值的单元格也会立即更新。

此外,其中一些属性是以异步方式计算的。例如,如果您的属性是文章 ID,您可能希望从后端下载该文章的标题和作者。这两个新属性——title 和 author——直接依赖于之前的属性——文章 ID。但它们无法以同步方式计算。我们需要发出异步 HTTP 请求,等待响应,处理可能发生的任何错误,然后才能更新 title 和 author 属性。

当你开始深入研究 Dipper 时,你会发现 Mobx 拥有丰富的机制和工具来处理这些情况,而且 Mobx 文档明确鼓励这种编程风格。你开始意识到 Mobx 只是表面上面向对象,实际上它遵循着一种完全不同的理念。

更重要的是,事实证明,在足够大的应用程序中,这个属性及其依赖关系图很快就会变得非常复杂。

如果您曾经见过庞大而复杂的 Excel 文件,以至于每个人都不敢对其进行任何更改 - 您基本上见过 Mobx 应用程序。

但除此之外,Mobx 的反应式机制对于开发人员来说无法直接访问或可见。正如我们所说,它隐藏在类、方法和装饰器的 OOP 语法之下。

正因如此,Mobx 的很多功能在程序员看来简直就是“魔术”。我花了很多时间绞尽脑汁,试图弄清楚为什么在特定情况下,Mobx 的机制会执行(或不执行)某些更新。我曾遇到过代码莫名其妙地发送多个 HTTP 请求而不是一个的情况。也曾遇到过代码没有发送任何请求的情况,尽管我确信它应该发送。

当然,最终错误总是在我这边。Mobx 运行正常。

但是,尽管 Redux 很复杂,因为它基本上将所有部分都交到您手中并要求您管理它们,但 Mobx 却恰恰相反,它向您隐藏了它的复杂性并假装它只是一个“常规”的 OOP 库。

一种方法会导致代码充满样板、多个文件并且难以跟踪代码库不同部分之间的关​​系。

第二种方法使代码看起来简洁而优雅,但有时​​它会做一些你意想不到的事情并且很难分析,因为你实际上不明白这个库在下面做了什么。

国家管理的谎言

有趣的是,整篇文章都是在共享状态是许多现代 Web 应用程序的共同要求的前提下编写的。

但...真的吗?

我的意思是,当然,有时您必须在应用程序的两个完全不同的位置显示许多未读消息。

但这真的足以成为创建复杂状态管理解决方案的理由吗?

也许……也许我们需要的只是一种以可管理的方式在组件之间共享状态的方法?

我想象有一个useSharedState钩子,它可以像常规的 React 状态钩子一样工作,但允许组件访问相同的状态实例,例如通过共享预定义键:

const [count, setCount] = useSharedState(0, "UNREAD_MESSAGES_COUNT");
Enter fullscreen mode Exit fullscreen mode

其实这个想法并不新鲜。我至少见过几个类似的钩子的实现。

人们似乎(有意识或无意识地)感觉到需要这种解决方案。

当然,它还不能解决所有问题。最大的问题是异步代码(尤其是数据获取)在现代 React 中仍然非常笨拙,用现代钩子语法实现它感觉就像是一种 hack(事实上,我可能会写一篇关于这个问题的后续文章)。

但我仍然坚持我在文章开头向你承诺的有争议的主张:

所有这些关于状态管理的争论、成千上万的库的创建和文章的撰写,主要源于一个原因——React 中没有简单的方法在组件之间共享状态实例。

请记住——我从未有机会用这个假设的useSharedState钩子编写一个完整的商业应用程序。正如我提到的,为了让这样的应用程序真正易于开发和维护,仍然需要做一些事情。

所以我现在说的一切可能都是完全错误的,但我还是要说:

我们在 React 中过度设计了状态管理。

在 React 中处理状态已经接近于一次很棒的体验 - 将状态与视图分离是一个巨大的垫脚石 - 我们只缺少一些针对非常具体的问题的小解决方案,例如共享状态或获取数据。

我们不需要状态管理框架和库。我们只需要对核心 React 机制进行一些调整(或者只是在外部库中添加一些小工具)。

编写大型 Web 应用程序总是很复杂。状态管理非常困难。事实上,你的应用程序越大,难度就越大。

但我相信,学习、调试和掌握状态管理库所花费的所有时间和精力都可以用来重构应用程序、更仔细地构建应用程序以及更好地组织代码。

这将使代码变得更简单、更易于理解、更易于整个团队管理。

我发现 React 社区已经在慢慢地发生这种转变,越来越多的人对使用 Redux 或 Mobx 进行编程感到失望。

那么...我今天用什么?

当然,Redux 和 Mobx 仍然有它们的地位。它们确实是很棒的库。它们解决了非常具体的问题,并带来了具体的优势(同时也带来了具体的缺点)。

如果您想涉足时间旅行调试或者需要将可序列化状态存储在一个地方(例如将其保存在后端或本地存储中),那么 Redux 就适合您。

如果您的应用程序状态高度互联,并且您希望确保一个属性的更新将导致其他属性的立即更新,那么 Mobx 模型将非常适合该问题。

如果您没有任何特殊要求,只需从原始 React 开始即可。

我在那篇文章中描述了“原生 React”方法的一些问题,但在实践中亲自遇到这些问题是完全不同的。有了这些经验,你将能够更好地选择合适的状态管理解决方案。

或者不选择。;)

如果您喜欢这篇文章,请在Twitter上关注我,我会定期发布有关 JavaScript 编程的文章。

感谢阅读!

(封面照片由Felix MittermeierUnsplash上拍摄)

链接:https://dev.to/mpodlasin/my-thoughts-on-endless-battle-of-react-state-management-libraries-setstate-usestate-vs-redux-vs-mobx-2kal
PREV
通过一个简单的示例深入解释 React.useEffect hook
NEXT
JS 中的函数式编程,第一部分 - 组合(Currying、Lodash 和 Ramda)