这是用户在 2024-6-17 10:52 为 https://tkdodo.eu/blog/react-19-and-suspense-a-drama-in-3-acts 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Skip to content
TkDodo's blog

React 19 and Suspense - A Drama in 3 Acts
React 19 与悬疑 - 三幕剧

ReactJs, React Query, Suspense, JavaScript7 min read
2024 年 6 月 16 日 — ReactJs、React Query、Suspense、JavaScript — 阅读 7 分钟

Silhouette of person under gray sky
Photo by Jr Korpa 摄影:Jr Korpa

That was quite a roller-coaster last week 🎢. Some things unravelled, some things went down, and in the middle of it: React Summit, the biggest React conference in the world.
上周的情况简直就像坐过山车一样。有些事情解开了,有些事情失败了,而在这期间:React Summit,世界上最大的 React 会议。

Let me try to break down what happened, in hopefully the right order, and what we can all learn from it. To do that, we have to go back to April this year:
让我尝试以正确的顺序分解发生的事情,以及我们可以从中学到什么。为此,我们必须回到今年四月:

First Act: React 19 Release Candidate
第一幕:React 19 候选版本

The 25th of April was a great day: React announced the React 19 RC - a release specifically for collecting feedback and preparing libraries for the next major version of React.
4 月 25 日是一个伟大的日子:React 发布了 React 19 RC - 该版本专门用于收集反馈并为 React 的下一个主要版本准备库。

I was really excited - there are so many good things in that release. From the new hooks to the use operator, from server actions to the ref prop. From better hydration errors to cleanup functions for refs. From better useRef types to useLayoutEffect finally not warning anymore on the server. And of course: The experimental React Compiler. 🚀
我真的很兴奋 - 该版本中有很多好东西。从新的钩子到 use 运算符,从服务器操作到 ref 属性。从更好的水合错误到参考的清理功能。从更好的 useRef 类型到 useLayoutEffect 最终不再在服务器上发出警告。当然还有实验性的 React 编译器。 🚀

This release is packed with goodies, and I was excited to upgrade React Query to see if there were any problems. I was quite busy at the time with work and finishing the 🔮 query.gg course, but about a month later, we released v5.39.0, which is compatible with React 19:
这个版本充满了好东西,我很高兴升级 React Query 以查看是否有任何问题。当时我工作很忙,完成了🔮query.gg课程,但大约一个月后,我们发布了v5.39.0,它兼容React 19:

Avatar for TkDodo
Dominik 🔮 多米尼克🔮
@TkDodo

React Query 🤝 React 19
反应查询🤝反应 19

github.com/TanStack/query...

Someone please try out the react compiler on an example and report back. I've enabled the eslint-plugin-react-compiler and it didn't report anything suspicious. I hope that means we play by the rules 😂
请有人在示例中尝试一下反应编译器并报告。我已经启用了 eslint-plugin-react-compiler,它没有报告任何可疑的内容。我希望这意味着我们遵守规则😂

New Release v5.39.0

- May 25, 2024 -2024 年 5 月 25 日

There weren't really any issues to dig into, so I thought this release was on track to become the best React release since hooks were introduced. That is, until we noticed something weird with suspense.
实际上没有任何需要深入研究的问题,所以我认为这个版本有望成为自引入 hooks 以来最好的 React 版本。也就是说,直到我们注意到一些奇怪的悬念。

Second Act: Uncovering Suspense
第二幕:揭开悬念

Full disclosure upfront: I wasn't the first to discover this. Shout out to Gabriel Valfridsson who (to the best of my knowledge) first spotted the new behaviour one day after the RC announcement:
预先全面披露:我不是第一个发现这一点的人。向 Gabriel Valfridsson 致敬,他(据我所知)在 RC 公告一天后首次发现了这种新行为:

Avatar for GabbeV_
Gabriel Valfridsson 加布里埃尔·瓦尔弗里森
@GabbeV_

A ton of great changes! 🥳
发生了很多巨大的变化! 🥳

However this change probably deserve a bigger disclaimer.
然而,这一变化可能值得更大的免责声明。

https://github.com/facebook/react...

Code using suspense with libraries like react-query will get waterfalls where loading previously happened in parallel.
将 Suspense 与 React-Query 等库一起使用的代码将在以前并行加载的地方获得瀑布式加载。

https://codesandbox.io/p/devbox/react...
https://codesandbox.io/p/devbox/react...

- Apr 26, 2024 - 2024 年 4 月 26 日

It's funny because I saw the tweet, and even commented on it, but didn't think too much of it at the time. As I said, I was quite busy and planned to look into React 19 later.
这很有趣,因为我看到了这条推文,甚至评论了它,但当时并没有想太多。正如我所说,我很忙并计划稍后研究 React 19。

So after the React 19 upgrade in React Query itself, I continued working on the suspense lesson of the course. We have one example in there were we're showing how to reveal content at the same time, but still have all requests fetch in parallel. As shown in the react docs, we can achieve this by by putting both components as siblings into the same suspense boundary. The example looks roughly like this:
因此,在 React Query 本身的 React 19 升级之后,我继续研究课程的悬念课。我们有一个示例,我们展示了如何同时显示内容,但仍然并行获取所有请求。如反应文档所示,我们可以通过将两个组件作为兄弟组件放入相同的悬念边界来实现这一点。该示例大致如下所示:

suspense-with-two-children
两个孩子的悬念
1export default function App() {
2 return (
3 <Suspense fallback={fallback={<p>...</p>}}>
4 <RepoData name="tanstack/query" />
5 <RepoData name="tanstack/table" />
6 </Suspense>
7 )
8}

The way this works is that React sees that the first child will suspend, so it knows that it has to show the fallback. However, it still continues to render other siblings in case they will also suspend, so that it can "collect" all promises.
其工作原理是,React 看到第一个子级将挂起,因此它知道它必须显示 fallback 。然而,它仍然继续渲染其他兄弟姐妹,以防它们也暂停,以便它可以“收集”所有承诺。

This is a pretty great feature because it means if each sibling triggers anything async, we can compose our components in a way that they will still fetch in parallel while not triggering a 🍿 "popcorn UI" 🍿 where multiple parts of the screen pop in one after the other.
这是一个非常棒的功能,因为这意味着如果每个兄弟触发任何异步内容,我们可以以一种方式组合我们的组件,它们仍然会并行获取,同时不会触发“爆米花 UI”🍿,其中屏幕的多个部分弹出在另一个之后。

A more complete example might look something like this:
更完整的示例可能如下所示:

app-with-suspense 悬念应用程序
1export default function App() {
2 return (
3 <Suspense fallback={<p>...</p>}>
4 <Header />
5 <Navbar />
6 <main>
7 <Content />
8 </main>
9 <Footer />
10 </Suspense>
11 )
12}

Some or all of those components can initiate critical data fetching, and we'll get our UI displayed at once when those fetches have resolved.
这些组件中的部分或全部可以启动关键数据获取,当这些获取解决后,我们将立即显示我们的 UI。

Another advantage is that we can add fetches later without having to think about how pending states will be handled. The <Footer /> component might not fetch data now, but if we add it later, it will just work. And if we deem data as non-critical, we can always wrap our component in it's own suspense boundary:
另一个优点是我们可以稍后添加提取,而不必考虑如何处理挂起状态。 <Footer /> 组件现在可能无法获取数据,但是如果我们稍后添加它,它就会正常工作。如果我们认为数据不重要,我们总是可以将组件包装在它自己的悬念边界中:

nested-suspense 嵌套悬念
1export default function App() {
2 return (
3 <Suspense fallback={<p>...</p>}>
4 <Header />
5 <Navbar />
6 <main>
7 <Content />
8 </main>
9 <Suspense fallback={<p>...</p>}>
10 <Footer />
11 </Suspense>
12 </Suspense>
13 )
14}

Now fetching data in our footer will not block rendering the main content. This is pretty powerful and aligned with how React favors component composition above anything else.
现在,在页脚中获取数据将不会阻止渲染主要内容。这非常强大,并且与 React 优先考虑组件组合的方式一致。


I vaguely remembered seeing something on twitter about suspense having a different behaviour in React 19, so just to be sure, I wanted to try out what we have in the course with the new RC release. And, to my surprise, it behaved completely differently: Instead of fetching data for both siblings in parallel, it now created a waterfall. 💦
我依稀记得在 Twitter 上看到过一些关于 React 19 中悬念有不同行为的内容,所以为了确定一下,我想尝试一下我们在新 RC 版本的课程中提供的内容。而且,令我惊讶的是,它的行为完全不同:它现在创建了一个瀑布,而不是并行获取两个兄弟姐妹的数据。 💦

I was so surprised by this behaviour that I did the only thing I could think of at that moment - I jumped on twitter and tagged some react core team members:
我对这种行为感到非常惊讶,以至于我做了当时唯一能想到的事情 - 我跳上 Twitter 并标记了一些 React 核心团队成员:

Avatar for TkDodo
Dominik 🔮 多米尼克🔮
@TkDodo

Am I imagining things or is there a difference between React 18 and 19 in terms of how Suspense handles parallel fetching? In 18, there is a "per component" split, so putting two components into the same Suspense Boundary, where each was doing a fetch, was still firing them in parallel:
我是在想象事情还是 React 18 和 19 在 Suspense 如何处理并行获取方面有区别吗?在 18 中,存在“每个组件”拆分,因此将两个组件放入同一个 Suspense Boundary 中,每个组件都在执行提取操作,但仍然并行触发它们:

This fires two queries, in parallel, waits until both are resolved and then shows the whole sub-tree.
这会并行触发两个查询,等待两个查询都得到解析,然后显示整个子树。

In React 19, as far as I can see, the queries run in a waterfall now. I think I remember @rickhanlonii mentioning something like this but I can't find any evidence now.
在 React 19 中,据我所知,查询现在以瀑布形式运行。我想我记得@rickhanlonii 提到过类似的事情,但我现在找不到任何证据。

/cc @acdlite @dan_abramov2

- Jun 11, 2024 -2024 年 6 月 11 日

Needless to say, this tweet took off and started a somewhat heated twitter discussion. We soon found out that this was not a bug, but an intentional change, which led to quite some outrage.
不用说,这条推文一炮而红,引发了推特上的热烈讨论。我们很快发现这不是一个错误,而是一个故意的改变,这引起了相当多的愤怒。

Why would they do that?
他们为什么要那样做?

There are of course reasons why this change was made, and, oddly enough, they are meant as a performance improvement for some situations. Continuing to render siblings of a component that has already suspended is not for free, and it will block showing the fallback. Consider the following example:
做出这种改变当然是有原因的,而且奇怪的是,它们是为了某些情况下的性能改进。继续渲染已经挂起的组件的同级组件并不是免费的,并且它会阻止显示回退。考虑以下示例:

expensive-sibling 昂贵的兄弟姐妹
1export default function App() {
2 return (
3 <Suspense fallback={<p>...</p>}>
4 <SuspendingComponent />
5 <ExpensiveComponent />
6 </Suspense>
7 )
8}

Let's assume that <ExpensiveComponent /> takes some time to render, e.g. because it is a huge sub-tree, but does not suspend itself. Now when react renders this tree, it will see that <SuspendingComponent /> suspends, so the only thing it will need to display eventually is the suspense fallback. However, it can only do that when rendering has finished, so it has to wait until <ExpensiveComponent /> is done rendering. Even more - the render result of <ExpensiveComponent /> will be thrown away, because the fallback has to be displayed.
假设 <ExpensiveComponent /> 需要一些时间来渲染,例如因为它是一个巨大的子树,但不会自行悬挂。现在,当 React 渲染这棵树时,它会看到 <SuspendingComponent /> 挂起,因此它最终需要显示的唯一内容是悬念回退。但是,它只能在渲染完成后执行此操作,因此必须等到 <ExpensiveComponent /> 渲染完成。更重要的是 - <ExpensiveComponent /> 的渲染结果将被丢弃,因为必须显示后备。

When we think about it this way - pre-rending the siblings of a suspended component is pure overhead, as it will never amount to a meaningful output. So React 19 removed that to get instant loading states.
当我们这样思考时 - 预渲染挂起组件的同级组件纯粹是开销,因为它永远不会产生有意义的输出。因此 React 19 删除了它以获得即时加载状态。

Of course, if you suspend instantly, you can't see that the siblings will also suspend, so if those siblings were to initiate data fetches (e.g. with useSuspenseQuery), they will now waterfall. And that's where the controversy comes in.
当然,如果您立即挂起,您看不到兄弟姐妹也会挂起,因此如果这些兄弟姐妹要启动数据获取(例如使用 useSuspenseQuery ),它们现在将瀑布流。这就是争议的根源。

Fetch-on-render vs. Render-as-you-fetch
渲染时获取与获取时渲染

Having a component initiate a fetch is usually called fetch-on-render. It's the approach most of us likely use on a daily basis, but it's not the best thing you can do. Even when siblings inside the same suspense boundary are pre-rendered in parallel, you would not be able to avoid the waterfall if you have two useSuspenseQuery calls within the same react component, or if you had a parent-child relationship between components.
让组件发起获取通常称为渲染时获取。这是我们大多数人每天都会使用的方法,但这并不是最好的方法。即使同一悬念边界内的兄弟姐妹并行预渲染,如果您在同一个 React 组件中有两个 useSuspenseQuery 调用,或者组件之间有父子关系,您也将无法避免瀑布。

That is why the recommended approach by the react team is to initiate fetches earlier, e.g. in route loaders or in server components, and to have suspense only consume the resource rather than initiate the promise itself. This is usually called render-as-you-fetch.
这就是为什么反应团队推荐的方法是尽早启动获取,例如在路由加载器或服务器组件中,并且悬念仅消耗资源而不是发起承诺本身。这通常称为“按获取即渲染”。

For example, with TanStack Router and TanStack Query, the example could look like this:
例如,对于 TanStack Router 和 TanStack Query,示例可能如下所示:

prefetch-in-route-loader 路由加载器中预取
1export const Route = createFileRoute('/')({
2 loader: ({ context: { queryClient } }) => {
3 queryClient.ensureQueryData(repoOptions('tanstack/query'))
4 queryClient.ensureQueryData(repoOptions('tanstack/table'))
5 },
6 component: () => (
7 <Suspense fallback={<p>...</p>}>
8 <RepoData name="tanstack/query" />
9 <RepoData name="tanstack/table" />
10 </Suspense>
11 ),
12})

Here, the route loader makes sure that the fetches for both queries are initiated before the component is rendered. So when react starts to render the suspense children, it doesn't matter if it renders the second RepoData component or not, because it wouldn't trigger a fetch - it would just consume the already running promise. In this situation, React 19 would make our app slightly faster because it would have to do less work without any drawbacks.
在这里,路由加载器确保在渲染组件之前启动两个查询的获取。因此,当 React 开始渲染 Suspense 子组件时,是否渲染第二个 RepoData 组件并不重要,因为它不会触发 fetch - 它只会消耗已经运行的 Promise。在这种情况下,React 19 将使我们的应用程序稍微快一些,因为它必须做更少的工作,而且没有任何缺点。

Not everything is a fetch
并非一切都是可取的

Hoisting your data requirements is a good idea regardless of how suspense works, and I also recommend doing that. However, with the proposed React 19 changes, it becomes almost mandatory to do so.
无论悬念如何运作,提高数据要求都是一个好主意,我也建议这样做。然而,随着 React 19 提议的更改,这样做几乎成为强制性的。

Further, if learned anything from React Query, it's that not every async operation is a fetch. For example, using React.lazy for code-splitting would also mean that bundles are loaded in serial if your App looks like this:
此外,如果从 React Query 中学到什么,那就是并非每个异步操作都是获取。例如,如果您的应用程序如下所示,使用 React.lazy 进行代码分割也意味着捆绑包会串行加载:

react.lazy 反应惰性
1const Header = lazy(() => import('./Header.tsx'))
2const Navbar = lazy(() => import('./Navbar.tsx'))
3const Content = lazy(() => import('./Content.tsx'))
4const Footer = lazy(() => import('./Footer.tsx'))
5
6export default function App() {
7 return (
8 <Suspense fallback={<p>...</p>}>
9 <Header />
10 <Navbar />
11 <main>
12 <Content />
13 </main>
14 <Footer />
15 </Suspense>
16 )
17}

Yes, you can technically preload dynamic imports as well, but making this required for good performance kind of defeats the purpose of react suspense and component composition, as the App component would need to know everything async that goes on in any of its children.
是的,从技术上讲,您也可以预加载动态导入,但是为了获得良好的性能而需要这样做,有点违背了反应悬念和组件组合的目的,因为 App 组件需要知道其任何子组件中发生的所有异步情况。

Third Act: Escalation and Delaying the Release
第三幕:升级并延迟发布

By now, a lot of people on the internet were surprised about and afraid of those changes. Screenshots were shared about how apps that fetched almost everything in parallel in 18 resulted in a total waterfall in 19. The developers behind the Poimandres open source developer collective, which maintains react-three-fiber, were a bit freaked out because a lot of what react-three-fiber is doing is based on async work and leverages how suspense works today. This went so far that even forking react was up for discussion if the change actually made it into v19.
到目前为止,互联网上的很多人对这些变化感到惊讶和害怕。屏幕截图显示,在 18 中并行获取几乎所有内容的应用程序如何在 19 中导致完全瀑布式增长。维护 React-三纤维的 Poimandres 开源开发人员集体背后的开发人员有点害怕,因为很多东西React-Three-Fiber 正在做的事情是基于异步工作并利用当今悬念的工作方式。事情发展到了如此地步,以至于即使是 fork React 也有待讨论,如果这个改变真的进入了 v19 的话。

By that time, I was already in Amsterdam for ReactSummit. We were talking about this change at the React Ecosystem Contributors Summit, where everyone was either surprised, concerned or frustrated. The React core team was doubling down, explaining how this change is the better tradeoff and raises the ceiling, how we should hoist data requirements anyways, and that official suspense support on the client was never released (which, even if true, everyone I know misunderstood).
那时,我已经在阿姆斯特丹参加 ReactSummit。我们在 React 生态系统贡献者峰会上讨论了这一变化,每个人都感到惊讶、担忧或沮丧。 React 核心团队加倍努力,解释了这种变化如何是更好的权衡并提高了上限,无论如何我们应该如何提升数据要求,以及客户端上的官方悬念支持从未发布(即使这是真的,我认识的每个人都知道)误解)。


Later that evening, I had the chance to talk to Sathya Gunasekaran, who worked on the react compiler and v19:
那天晚上晚些时候,我有机会与 Sathya Gunasekaran 交谈,他负责 React 编译器和 v19:

Avatar for TkDodo
Dominik 🔮 多米尼克🔮
@TkDodo

Had a great discussion with @_gsathya about React19, the suspense changes and the react compiler. I felt like my concerns were truly heard and the feedback appreciated, so thank you for that 🙏
与 @_gsathya 就 React19、悬念变化和 React 编译器进行了精彩的讨论。我觉得我的担忧得到了真正的倾听,反馈也得到了重视,所以谢谢你们🙏

Sathya and me

- Jun 13, 2024 -2024 年 6 月 13 日

He ensured me that the react team cares a lot about the community and that they likely underestimated how the change influences client side suspense interactions.
他向我保证,反应团队非常关心社区,并且他们可能低估了这一变化对客户端悬念交互的影响。

On the next day, the react team met and decided to hold the release:
第二天,React 团队开会并决定举行发布:

Avatar for sophiebits
sophie alpert 苏菲·阿尔珀特
@AvatarSophiebits

good news re Suspense, just met w/ @rickhanlonii @en_JS @acdlite
关于悬念的好消息,刚刚与@rickhanlonii @en_JS @acdlite 见面

* we care a lot about SPAs, team misjudged how many people rely on this today
* 我们非常关心 SPA,团队错误地判断了今天有多少人依赖它

* still recommend preloading but recognize not always practical
* 仍然建议预加载,但认识到并不总是实用

* we plan to hold the 19.0 release until we find a good fix
* 我们计划保留 19.0 版本,直到找到好的修复方案

- Jun 14, 2024 - 2024 年 6 月 14 日

It's very reassuring that the react team is open to feedback at this stage. Postponing a release that was already announced and presented at a conference is a big move - one that everyone involved really appreciated. I'll gladly work with the react team as best I can to find good compromise in that matter.
令人非常放心的是,React 团队现阶段愿意接受反馈。推迟已经宣布并在会议上发布的版本是一个重大举措 - 每个参与者都非常赞赏这一举措。我很乐意与 React 团队合作,尽我所能,在这件事上找到良好的妥协方案。

Learnings 学习内容

There are a couple of things I learned from all of this. For one, trying out early releases before they come out as a final version is a very good idea, especially if the team is ready to take feedback and act on it. Kudos to the react team - I just really wish I would've given that feedback earlier.
我从这一切中学到了一些东西。首先,在最终版本发布之前尝试早期版本是一个非常好的主意,特别是如果团队准备好接受反馈并采取行动的话。感谢 React 团队 - 我真的希望我能早点给出反馈。

The other thing that became obvious to me and other maintainers is that we need a better channel to communicate with the react team. The React 18 Working Group was probably the best thing we had in this regard, and this whole saga shows that having something similar for React 19 (and future react releases) would be great. Something like a permanent working group maybe?
对我和其他维护人员来说显而易见的另一件事是,我们需要一个更好的渠道来与 React 团队沟通。 React 18 工作组可能是我们在这方面拥有的最好的东西,整个传奇表明,React 19(以及未来的 React 版本)拥有类似的东西会很棒。也许是一个常设工作组之类的东西?

Also, obvious but worth mentioning: shouting at each other on twitter is not helpful. I regret the part I took in any communication that wasn't calm and objective, and I really appreciate Sophie's way of communicating and handling things. 🙏
另外,显而易见但值得一提的是:在推特上互相喊叫是没有帮助的。我很遗憾自己在沟通中表现得不够冷静和客观,而且我真的很欣赏索菲的沟通和处理事情的方式。 🙏

Like so many others before me have figured out: interactions in person are always so much better, and I'm looking forward to having more great conversations at conferences. 🎉
就像我之前的许多人已经意识到的那样:面对面的互动总是更好,我期待在会议上进行更多精彩的对话。 🎉


That's it for today. Feel free to reach out to me on twitter if you have any questions, or just leave a comment below. ⬇️
今天就这样。如果您有任何疑问,请随时在 Twitter 上与我联系,或者在下面发表评论。 ⬇️

Like the monospace font in the code blocks?
喜欢代码块中的等宽字体吗?
Check out monolisa.dev 查看 monolisa.dev
Bytes - the JavaScript Newsletter that doesn't suck
Drag to outliner or Upload
Close