首先必须要明确一个观点:React 19 让网站性能变慢,这是片面且没有具体依据的表达。
前因
月初的时候,有很多博主都在发文说:React 19 让网站性能变慢了。我确实刚开始的时候并没有太过于放在心上,毕竟文章内容内容我也看了,也不太经得起推敲。但是没想到群里很多朋友都被此影响,认为 react 19 问题非常大。昨晚直播的时候,有朋友跟我连麦说:"我现在对 React 19 失去了期待和热情"。
所以,我感觉我自己得写一篇文章抢救一下 React 19 在大家心中的形象。毕竟,作为一个国内为数不多的 React 19 资深使用者,我确实感觉它真的很好用。我会在文中为大家分享 React 19 中所提倡的架构思维,让大家能够理解到 React 19 的正确使用姿势。,绝对爽得飞起,不爽你拿刀来砍我!
争议的起因与分析
TanStack Query 的核心开发者之一的大佬 Dominik
在推特上发文吐槽 React 19 中 Suspense
如果内嵌两个异步组件,会导致这两个组件的请求变成串行请求,从而让内容出现的速度变得更慢
✓串行请求:多个接口依次请求,上一个请求完成之后,下一个再请求
与之对应概念叫做并行请求,即多个接口可以同时请求
React 19 Suspense 的这个特性,在我之前的文章【React 19 出手解决了异步请求的竞态问题,是好事,还是坏事】
也为大家分享了 Suspense 的这个串行的特性。
不过,这里其实他的吐槽里面有一个很重要的问题就是,在 React19 之前的版本中,React 官方团队从未正式提出过一种方案,可以允许开发者在 React 中使用 Suspense 去处理请求数据的异步组件。
问题就出在这里。
在 React 19 的版本之前,如何正确的使用 Suspense 其实是还处于一个摸索中的话题。一些三方的请求库都设计了自己的用法,例如 react query,例如 Jotai。当 React 19 出现之后,官方团队设想的 Suspense 用法的实践与这些三方库自己摸索的用法不一样,于是矛盾就产生了。
Dominik
希望 Suspense 可以这样做,并且能够支持并行的请求。
但是很显然,在 React 官方文档中,除了嵌套案例之外,所有的案例都是只在 <Suspense>
的子组件中只包含一个单一的异步子元素。
也就意味着,如果你并没有使用这些三方库,你应该也不会使用 Suspense,你几乎感受不到这种变化的影响。因此,说 React 19 让网站性能变慢,是非常不客观的。 这种矛盾只是大家各位为自己提出来的开发理念进行争议,而不是说,Dominik
的使用就一定是最佳实践。
✓在开发理念上存在这种争议是非常正常的事情,例如在之前我为了给自己在 React 哲学中提出的开关思维进行辩护时,我也并不认同 React 官方文档中 useEffect 关于逃生舱的说法与建议。
那么在这一次的争议中,从我个人的角度出发,react-query
并不好用,他虽然功能强大,但是概念太多,使用起来也并不简洁,学习成本也并不低。如果你熟悉并习惯了 React 19 的开发思维,你大概也会跟我有同样的感受。
接下来,我会详细给大家介绍一下 React 19 中新提出来的开发理念。
理念一:将数据存储在 promise 中,并传递 promise
这种理念在几年前只流传在少部分资深程序员之间,由于过于超前,因此很少被人理解。也就没有传播开。但是,在我写 React 19 小册的过程中,有许多人跟我沟通,我发现国内有不少大佬在这之前就在这样使用了。这些大佬大多数都是大厂里的高手。
React 19 中,正式提出 use
hook,可以通过正规渠道从 promise 中获取数据,表示这种理念将会被正式确定,并逐渐被更多的人接纳。
例如这样一段代码,我们往子组件中传入获取数据的 promise,并在子组件中通过 use
获取到请求结果
这是一个仅包含初始化的案例,演示如图所示
你会发现,这种写法,最大的优势就是,初始化不再是一个副作用逻辑。我们不再依赖于 useEffect 去获取初始化的数据。
更妙的地方在于,如果我要更新数据重发请求,我只需要做一些小小的改动,那就是将 promise 存储在 state 中即可。在更新时重置 promise,注意看下面这段代码。
演示效果:
这里一个非常厉害的地方就在于,我们用简洁的代码,在不使用 useEffect 的情况之下,完成了初始化与更新的交互。这就是这种开发理念的巧妙之处。
并且这也可以应对足够多的场景,例如,有的时候,我们并不想立即初始化,那么我们只需要稍作调整即可
当我们想要初始化的时候,那么就在恰当的时候调用如下语句即可
另外一个非常巧妙的地方在于,当我们在更新时,如果参数比较复杂,我们处理起来也并不麻烦,因为我们可以在更新 promise 时,直接执行 getMessage(params)
传入参数。和之前的方案相比,这极大的简化了请求参数的运用,现在变得更加自然。
很显然,在开发体验上,这种在 promise 中存储数据,并且将 promise 存储在 state 中的方式,一定是未来。
理念二:promise 作为独立的数据层
在上面的案例中,有一行代码不是很起眼,但是却非常重要。
这行代码所代表的含义,正是整个项目架构思维中最重要的一环。他代表了我们获取数据的方式。
如果当前的组件,只有一个接口请求去获取数据,大家都知道应该如何处理
但是如果当前组件需要两个接口请求的数据,我们应该怎么办呢?有的人就懵了。实际上我们依然应该践行同样的逻辑。我们需要抽象一下这个概念,对于页面组件而言,不管有几个接口,都应该只有一份数据来源。因此,我们应该在 getMessage
中完成数据的聚合。
聚合的方式根据需要自己来定,这两个接口可能有前后依赖关系,也有可能只需要 Promise.all
即可.
这样,我们可以在任何复杂的场景中,保持页面的简洁。对于组件而言,他依然只认 getMessage
这一个 promise,不会随着复杂度的变化而变得更加复杂。
在这样的开发理念之下,我们可以将该 promise 称为独立的数据层。这类似于许多团队在架构中引入的 node 层,专门用来处理数据的聚合与变化,抹平后台数据库数据结构与应用层数据结构的差异。
有的时候你可能需要缓存数据,也应该在我这里定义的数据层中,通过判断来获取。
这种思路可以极大的简化组件的开发思路。也就意味着,大多数情况下,一个组件中不应该出现两个获取数据的行为,因为这会导致情况处理起来变得复杂,我们只需要将两个接口放到 promise 中去处理就可以了。
也就意味着,在最佳实践中,每一个 Suspense 的子组件,都只应该只有一个,而不会出现两个或者更多。
总结
将数据直接存储在 promise 中,并将 promise 存在 state 上,通过改变和传递 promise 的方式来实现组件需求,是 React 19 明确引导的新开发理念,这种理念能够大量的减少我们对于 useEffect 的依赖。在开发体验上是一次质的飞跃。
而在此基础上,将 promise 独立抽象为数据层,则是架构思维的体现,这种架构思维与 React 19 的开发理念可以非常巧妙的结合在一起,产生异常强大的化学反应。他们能够极大的简化代码的复杂度,当然,还可以提高大家的开发效率。
当然也有坏处:那就是对于新手来说并不友好。最终的代码是简单的,但是理解成本却是很高的。底子不牢固的朋友可能很难第一时间就完全领悟,需要花时间去接受它们。
最后解答一个群有的疑问:我曾经在 React 哲学
中提出一个另外一种开发思维:开关思维。 React19 的开发理念,与开关思维有异曲同工之妙,从我个人的使用和体验上来看,并没有明确的高下之分,更多的可能只是使用习惯上的一些小差异,两种方案最终的结果都非常相似,都具备非常强的简洁性。但是这两者之间的思维逻辑是完全不一样的。开关思维强调的是将行为抽象为 on/off
这样的布尔值,通过控制布尔值来做到更新。而 React19 则更加直接一些。因此,两种思维模式都掌握的小伙伴,可以根据喜好随意选择一种即可。
彻底学会 React 19,推荐阅读我的付费小册 React19
成为 React 高手,推荐阅读 React 哲学