React v18.0

2022 年 3 月 29 日,作者:React 团队


React 18 现已在 npm 上发布!在我们上一篇文章中,我们分享了有关将您的应用程序升级到 React 18的逐步说明。在这篇文章中,我们将概述 React 18 中的新增内容及其对未来的意义。


我们最新的主要版本包含开箱即用的改进,例如自动批处理、startTransition 等新 API,以及支持 Suspense 的流式服务器端渲染。

React 18 中的许多功能都是基于我们新的并发渲染器构建的,这是一个幕后的更改,它解锁了强大的新功能。并发 React 是可选的 - 只有在您使用并发功能时才会启用它 - 但我们认为它会对人们构建应用程序的方式产生重大影响。

我们花了数年时间研究和开发对 React 中并发性的支持,并且格外小心地为现有用户提供了一个渐进式采用路径。去年夏天,我们组建了 React 18 工作组,从社区专家那里收集反馈,并确保整个 React 生态系统都能顺利升级。

如果您错过了,我们在 React Conf 2021 上分享了我们的大部分愿景。

以下是本版本中期待内容的全面概述,从并发渲染开始。

注意

对于 React Native 用户,React 18 将与新 React Native 架构一起在 React Native 中发布。有关更多信息,请参见React Conf 主题演讲

什么是并发 React?

React 18 中最重要的新增内容是希望您永远不必考虑的东西:并发。我们认为这对应用程序开发人员来说很大程度上是正确的,尽管对于库维护人员来说,情况可能有点复杂。

并发本身并不是一个功能。它是一种新的幕后机制,使 React 能够同时准备 UI 的多个版本。您可以将并发视为一个实现细节 - 因为它解锁的功能而变得有价值。React 在其内部实现中使用复杂的技巧,例如优先级队列和多缓冲。但您不会在我们的公共 API 中看到这些概念。

当我们设计 API 时,我们试图从开发人员那里隐藏实现细节。作为 React 开发人员,您专注于想要的用户体验,React 处理如何提供这种体验。因此,我们不希望 React 开发人员了解并发在幕后的工作原理。

然而,并发 React 比典型的实现细节更重要 - 它是 React 核心渲染模型的基础更新。因此,虽然了解并发的工作原理并不十分重要,但从高级别了解它是值得的。

并发 React 的一个关键特性是渲染是可以中断的。当您首次升级到 React 18 时,在添加任何并发功能之前,更新的渲染方式与 React 的先前版本相同 - 在单个、不间断的同步事务中。在同步渲染中,一旦更新开始渲染,在用户可以在屏幕上看到结果之前,没有任何东西可以中断它。

在并发渲染中,情况并非总是如此。React 可能会开始渲染更新,在中间暂停,然后继续。它甚至可能完全放弃正在进行的渲染。React 保证即使渲染被中断,UI 也会显示一致。为此,它会等到整个树都评估完毕后才执行 DOM 突变。凭借这种能力,React 可以准备后台的新屏幕,而不会阻塞主线程。这意味着即使 UI 正在进行大型渲染任务,它也可以立即响应用户输入,从而创造流畅的用户体验。

另一个例子是可重用状态。并发 React 可以从屏幕中删除 UI 的部分,然后在重用之前状态的同时将它们添加回来。例如,当用户从一个屏幕切换到另一个屏幕,然后返回时,React 应该能够以之前状态恢复之前的屏幕。在即将发布的次要版本中,我们计划添加一个名为<Offscreen>的新组件来实现此模式。类似地,您将能够使用 Offscreen 在后台准备新的 UI,以便在用户显示它之前准备好。

并发渲染是 React 中一个强大的新工具,我们的大多数新功能都是为了利用它而构建的,包括 Suspense、过渡和流式服务器端渲染。但 React 18 只是我们希望在这个新基础上构建的开始。

逐步采用并发功能

从技术上讲,并发渲染是一个重大更改。由于并发渲染是可以中断的,因此组件在启用它时行为略有不同。

在我们的测试中,我们已经将数千个组件升级到 React 18。我们发现几乎所有现有组件在没有进行任何更改的情况下都可以与并发渲染“正常工作”。但是,其中一些可能需要一些额外的迁移工作。尽管更改通常很小,但您仍然可以按照自己的节奏进行更改。React 18 中的新渲染行为仅在使用新功能的应用程序部分启用。

总体升级策略是在不破坏现有代码的情况下让您的应用程序在 React 18 上运行。然后您可以按照自己的节奏逐步开始添加并发功能。您可以使用<StrictMode>帮助在开发过程中发现与并发相关的错误。严格模式不会影响生产行为,但在开发过程中它会记录额外的警告并双重调用预期是幂等的函数。它不会捕获所有内容,但对于防止最常见的错误类型非常有效。

升级到 React 18 后,您将能够立即开始使用并发功能。例如,您可以使用 startTransition 在不阻塞用户输入的情况下在屏幕之间导航。或者使用 DeferredValue 来限制昂贵的重新渲染。

但是,从长远来看,我们预计您向应用程序添加并发的主要方式是使用支持并发的库或框架。在大多数情况下,您不会直接与并发 API 交互。例如,路由库将自动将导航包装在 startTransition 中,而不是让开发人员在每次导航到新屏幕时都调用 startTransition。

库升级以支持并发可能需要一些时间。我们提供了新的 API 使库更轻松地利用并发功能。在此期间,请耐心等待维护人员,因为我们正在努力逐步迁移 React 生态系统。

有关更多信息,请参阅我们之前的帖子:如何升级到 React 18

数据框架中的 Suspense

在 React 18 中,您可以开始使用 Suspense 来在 Relay、Next.js、Hydrogen 或 Remix 等有主见的框架中进行数据获取。虽然 Suspense 技术上支持临时数据获取,但我们不建议将其作为通用策略。

将来,我们可能会公开更多原语,使您更容易使用 Suspense 访问数据,也许不需要使用有主见的框架。但是,Suspense 在与您的应用程序架构深度集成时效果最佳:您的路由器、您的数据层和您的服务器渲染环境。因此,即使在长期内,我们也预计库和框架将在 React 生态系统中发挥至关重要的作用。

与以前版本的 React 相同,您也可以使用 Suspense 在客户端上使用 React.lazy 进行代码拆分。但我们对 Suspense 的愿景一直远不止加载代码——目标是扩展对 Suspense 的支持,以便最终,相同的声明式 Suspense 备用功能可以处理任何异步操作(加载代码、数据、图像等)。

服务器组件仍在开发中

服务器组件 是一项即将推出的功能,它允许开发人员构建跨越服务器和客户端的应用程序,将客户端应用程序的丰富交互性与传统服务器渲染的性能提升相结合。服务器组件本身并不与并发 React 耦合,但它旨在与 Suspense 和流式服务器渲染等并发功能配合使用效果最佳。

服务器组件目前仍处于实验阶段,但我们预计将在 18.x 版本的小版本中发布初始版本。在此期间,我们正在与 Next.js、Hydrogen 和 Remix 等框架合作,推动提案的进展,并使其准备好广泛采用。

React 18 中的新功能

新功能:自动批处理

批处理是指 React 将多个状态更新分组到单个重新渲染中以提高性能。如果没有自动批处理,我们只会在 React 事件处理程序中批处理更新。在 Promise、setTimeout、原生事件处理程序或任何其他事件中的更新在 React 中默认情况下不会被批处理。有了自动批处理,这些更新将被自动批处理。

// Before: only React events were batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will render twice, once for each state update (no batching)
}, 1000);

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);

有关更多信息,请查看有关 React 18 中的自动批处理以减少渲染次数 的这篇文章。

新功能:过渡

过渡是 React 中一个新概念,用于区分紧急更新和非紧急更新。

  • 紧急更新 反映直接交互,例如键入、单击、按下等。
  • 过渡更新 将 UI 从一个视图过渡到另一个视图。

紧急更新(例如键入、单击或按下)需要立即响应,以符合我们对物理物体行为的直觉。否则,它们会让人感觉“不对”。但是,过渡不同,因为用户不希望在屏幕上看到每个中间值。

例如,当您在下拉菜单中选择一个筛选器时,您希望筛选器按钮本身在您单击时立即做出响应。但是,实际结果可能会单独过渡。短暂的延迟将难以察觉,而且通常是可以预期的。如果您在结果完成渲染之前再次更改筛选器,您只关心看到最新的结果。

通常,为了获得最佳用户体验,单个用户输入应该会导致紧急更新和非紧急更新。您可以在输入事件中使用 startTransition API 来通知 React 哪些更新是紧急的,哪些是“过渡”。

import { startTransition } from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setSearchQuery(input);
});

使用 startTransition 包装的更新将被视为非紧急更新,如果出现更多紧急更新(例如单击或按键按下),它们将被中断。如果过渡被用户中断(例如,通过连续键入多个字符),React 将丢弃未完成的陈旧渲染工作,只渲染最新的更新。

  • useTransition: 一个 Hook 用于启动过渡,包括一个用于跟踪待处理状态的值。
  • startTransition: 一个方法,当无法使用 Hook 时用于启动过渡。

过渡将选择加入并发渲染,这允许更新被中断。如果内容重新挂起,过渡也会告诉 React 在后台渲染过渡内容的同时继续显示当前内容(有关更多信息,请参阅 Suspense RFC)。

有关过渡的文档,请参见此处.

新的 Suspense 功能

Suspense 允许您声明性地指定组件树一部分的加载状态,如果该部分尚未准备好显示。

<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>

Suspense 使“UI 加载状态”成为 React 编程模型中的一个一等声明式概念。这使我们能够在此基础上构建更高级的功能。

我们几年前引入了 Suspense 的一个有限版本。但是,唯一支持的用例是使用 React.lazy 进行代码拆分,并且在服务器上渲染时完全不支持它。

在 React 18 中,我们添加了对服务器上 Suspense 的支持,并使用并发渲染功能扩展了其功能。

React 18 中的 Suspense 在与过渡 API 结合使用时效果最佳。如果您在过渡期间挂起,React 将阻止已经可见的内容被备用内容替换。相反,React 将延迟渲染,直到加载了足够的数据以防止出现不良的加载状态。

有关更多信息,请参阅有关 React 18 中的 Suspense 的 RFC。

新的客户端和服务器渲染 API

在这个版本中,我们借此机会重新设计了我们在客户端和服务器上渲染时公开的 API。这些更改允许用户在他们升级到 React 18 中的新 API 时继续使用 React 17 模式下的旧 API。

React DOM 客户端

这些新 API 现在从 react-dom/client 中导出

  • createRoot: 用于创建根节点的新方法,用于 renderunmount。请使用它代替 ReactDOM.render。React 18 中的新功能无法在没有它的情况下使用。
  • hydrateRoot: 用于将服务器渲染的应用程序进行水合的新方法。请使用它代替 ReactDOM.hydrate,并结合使用新的 React DOM Server API。React 18 中的新功能无法在没有它的情况下使用。

Both createRoothydrateRoot 接受一个名为 onRecoverableError 的新选项,如果您希望在 React 从渲染或水合过程中的错误恢复时收到通知以进行日志记录,可以使用它。默认情况下,React 将使用 reportError,或者在较旧的浏览器中使用 console.error

此处查看 React DOM 客户端文档.

React DOM Server

这些新 API 现在从 react-dom/server 中导出,并完全支持在服务器上进行 Suspense 流式传输

  • renderToPipeableStream: 用于在 Node 环境中进行流式传输。
  • renderToReadableStream: 适用于现代边缘运行时环境,例如 Deno 和 Cloudflare Workers。

现有的 renderToString 方法仍然有效,但已不建议使用。

此处查看 React DOM Server 文档.

新的严格模式行为

将来,我们希望添加一项功能,允许 React 在保留状态的同时添加和删除 UI 部分。例如,当用户从一个屏幕切换到另一个屏幕并返回时,React 应该能够立即显示上一个屏幕。为此,React 将使用相同的组件状态卸载和重新挂载树。

此功能将为 React 应用程序提供开箱即用的更好性能,但需要组件能够适应多次挂载和销毁的效果。大多数效果无需任何更改即可工作,但某些效果假设它们只挂载或销毁一次。

为了帮助发现这些问题,React 18 在严格模式中引入了新的开发专用检查。此新检查将在组件首次挂载时自动卸载和重新挂载每个组件,并在第二次挂载时恢复先前状态。

在此更改之前,React 会挂载组件并创建效果

* React mounts the component.
* Layout effects are created.
* Effects are created.

在 React 18 中使用严格模式,React 将模拟在开发模式下卸载和重新挂载组件

* React mounts the component.
* Layout effects are created.
* Effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effects are created.
* Effects are created.

此处查看有关确保可重用状态的文档.

新的 Hook

useId

useId 是一个新的 Hook,用于在客户端和服务器上生成唯一 ID,同时避免水合不匹配。它主要用于与需要唯一 ID 的辅助功能 API 集成的组件库。这解决了一个在 React 17 及以下版本中已经存在的问题,但在 React 18 中由于新的流式服务器渲染器如何以乱序方式提供 HTML,因此这个问题变得更加重要。 此处查看文档

注意

useId 不是用于生成 列表中的键。键应从您的数据中生成。

useTransition

useTransitionstartTransition 允许您将某些状态更新标记为非紧急。默认情况下,其他状态更新被视为紧急。React 将允许紧急状态更新(例如,更新文本输入)中断非紧急状态更新(例如,渲染搜索结果列表)。 此处查看文档

useDeferredValue

useDeferredValue 允许您延迟重新渲染树的非紧急部分。它类似于防抖,但与之相比有一些优势。没有固定的时间延迟,因此 React 将尝试在第一次渲染反映到屏幕后立即执行延迟渲染。延迟渲染是可以中断的,并且不会阻止用户输入。 此处查看文档

useSyncExternalStore

useSyncExternalStore 是一个新的 Hook,它允许外部存储通过强制对存储的更新为同步的方式来支持并发读取。它消除了在实现对外部数据源的订阅时对 useEffect 的需求,并且建议用于任何与 React 外部状态集成的库。 在此处查看文档.

注意

useSyncExternalStore 旨在供库使用,而不是应用程序代码。

useInsertionEffect

useInsertionEffect 是一个新的 Hook,它允许 CSS-in-JS 库解决在 render 中注入样式的性能问题。 除非您已经构建了 CSS-in-JS 库,否则我们预计您永远不会使用它。 此 Hook 将在 DOM 被修改后运行,但在布局效果读取新布局之前。 这解决了一个在 React 17 及以下版本中已经存在的问题,但在 React 18 中更加重要,因为 React 在并发渲染期间会让步给浏览器,让浏览器有机会重新计算布局。 在此处查看文档.

注意

useInsertionEffect 旨在供库使用,而不是应用程序代码。

如何升级

有关分步说明和完整的重大更改和重要更改列表,请参阅 如何升级到 React 18

变更日志

React

React DOM

React DOM Server

React DOM 测试工具

  • 当在生产环境中使用 act 时抛出异常。 (#21686 by @acdlite)
  • 支持使用 global.IS_REACT_ACT_ENVIRONMENT 禁用虚假 act 警告。 (#22561 by @acdlite)
  • 扩展 act 警告以涵盖所有可能安排 React 工作的 API。 (#22607 by @acdlite)
  • 使 act 批量更新。 (#21797 by @acdlite)
  • 删除悬挂被动效果的警告。 (#22609 by @acdlite)

React 刷新

  • 在快速刷新中跟踪延迟安装的根节点。 (#22740 by @anc95)
  • package.json 文件中添加 exports 字段。 (#23087@otakustay 提交)

服务器组件(实验性)

  • 添加服务器上下文支持。 (#23244@salazarm 提交)
  • 添加 lazy 支持。 (#24068@gnoff 提交)
  • 更新 webpack 插件以支持 webpack 5 (#22739@michenly 提交)
  • 修复 Node 加载器中的错误。 (#22537@btea 提交)
  • 在 Edge 环境中使用 globalThis 而不是 window。 (#22777@huozhi 提交)