3 万字长文带你了解【React v18 】 到底带来了些什么玩意儿?

1,168 阅读39分钟

在研究了 React v18 的新 Features 之后,最近开始将部分项目在升级到了 React v18,刚好借这个契机,对 React v18 这个版本的知识体系做系统性的归纳整理,并深入学习一下各个新 Feature 在实际项目中的应用 ~

本文结合了包括 React官方React维护者(主要是Dan大神)的演讲和讨论社区优质的解读文章,试图对 React v18 这个版本做通俗易懂的介绍,后续还将对每个重要的新 Feature 单独做深入的理解和应用,敬请期待 ~

前言

React 在过去十年中取得了长足的进步,经历了重大变革,使其成为当今使用最广泛的前端 UI 框架之一。 

最初,React 冗长的语法给开发人员带来了使用上的挑战。随后,React 引入了 JSX 语法,简化了组件的创建。接下来,带有 Hook 的函数组件使处理状态和生命周期函数变得更加容易,从而产生更优雅、可读和可维护的代码。随着 React v16 中服务器端渲染 (SSR) 的引入,通过允许更快的初始页面加载时间和更好的搜索引擎优化 (SEO) 来提高性能。Next.jsRemix 等全栈框架使 React 变得更加易于使用,进一步提高了它的受欢迎程度。

2020年8月发布的 React 17,并没有带来新的功能,只是作为一个 【垫脚石】 版本,作为 React v18 中引入的新功能的关键准备。虽然 React v17 中的 React 开发者 API 没有进行任何更改,但它引入了增量升级版本的方法以及运行的能力单个组件树中存在多个版本的 React。这是实现 React v18 更新功能的重要一步。这个版本稳定 Concurrent 这一在底层带来 Breaking Change 的新机制,为未来无缝切换打下了基础。

image.png

React v17.0

React v18.0

在 2021 年 6 月 8 号,React 公布了 v18 版本的发布计划,并发布了 alpha 版本。经过将近一年的发布前准备,在 2022 年 3 月 29 日,React v18 正式发布,该版本重点关注性能改进渲染引擎更新

React v18 应该是最近几年的一个重磅版本,React 官方对它寄予了厚望。不然也不会将 React v17 作为一个过渡版本,也不会光发布准备工作就做了一年。React v18 为并发渲染 API 奠定了基础,未来的 React 功能将在此基础上构建。

React 18 概览

React 18 的核心新特性基本可以归为下面两类:

  1. 并发特性:为了解决大量DOM节点同时更新造成的渲染延迟问题,以及子组件IO操作被父组件阻塞的问题。React新的 Fiber Reconciler 通过将任务拆分成小块,支持了并发渲染。既:允许React同时渲染多版本的UI。
  2. 新SSR架构:允许用户将应用拆分为更小的单元,使每个单元具备独立的 fetch data (server) → render to HTML (server) → load code (client) → hydrate (client) 流程。

新 Features 速览:

分类Features
概念Concurrent React
新功能Automatic Batching, Transitions, Suspense on the server
APIscreateRoot, hydrateRoot, renderToPipeableStream, renderToReadableStream
HooksuseId, useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect
更新Strict mode
已弃用/不鼓励ReactDOM.render, renderToString

Concurrent Mode(并发)

Dan 大神在直播中的解释

在 2021 年「在线对话 React.js 核心开发者」一个半小时直播中,Dan 大神是这么说的:

主持人:有些朋友们问到了 concurrent mode 的相关问题。React 的 concurrent mode 自 16 版本就已经提出了,但到了 React 18 才终于发布了出来。React Team 在设计这个功能的时候遇到了哪些问题和挑战,你们对于这个功能在将来又有哪些计划呢?

Dan:好的,我首先要说明一下,React 18 还没有正式发布,我们目前只为库的维护者们发布了一个 alpha 版本。如果你访问 reactjs.org/blog,你会看到一篇名为《The Plan for React 18》的文章。如果朋友们想去了解 React 18 的新功能或者发布的时间计划可以去看一下。我们还建立了一个工作小组,并邀请了 50 名左右的社区开发者来协助我们完成一些 React 18 发布的相关工作,比如确保整个 React 生态中的库都能兼容新的版本。相关信息都在 GitHub Discussion 里面,非组员虽然不能评论,但是可以阅读。你可以从中找到很多关于发版的信息,其中就包括你问的 concurrent mode 的信息。

正如你所说,我们在很多年前就声明了要做真正的 concurrent mode,不过这期间有一个很大的策略上的变化,那就是它不再作为一个单独的模式(mode) 来呈现。所以我们最终添加到 React 中的可能并不是一个 concurrent mode,而是一种并发渲染的机制。本质上来说,并发意味着 React 有能力在同一时间更新多项状态。比如你在更新状态的时候,有些获取数据的状态更新会花费比较长的时间,渲染一个复杂页面的时间成本就会很高。如果使用了并发渲染,这些场景下 React 就不会阻塞住浏览器。这意味着页面在进行长时间的状态更新的同时,你还可以去输入文字,后台的状态更新不会阻塞你的交互。基本上,你可以把并发渲染想象成是在后台跑 setState()。这确实是我们一直想在 React 中实现的功能,但重要的是它不再是一个单独的模式了。

当你需要使用并发能力的时候,我们提供了一个叫 Transition 的功能,你可以用 Transition 包裹 setState() 的操作,这样在调用 API 的时候 React 就会进行并发渲染。所以这个在将来会是一个可以随时调用的特性,而不是作为一个 Web 应用的全局的模式,他只针对于你包裹的操作生效。 就挑战而言,我们确实遇到了不少挑战。因为我们是从理论出发的,我们得出了很多理论性的想法,比如我们认为「并发是好的」,「长时间阻塞浏览器是不合理的」,「渲染应该是可以被打断的」等等。然后我们基于这些想法着手去做,在 2015 年完成了一个原型,之后又重写了 React 来让它能够真正实现这些想法,这就是 React 16 版本。之后我们确实花费了几年的时间来思考如何能够将这些功能应用到实际生产中。全新的 Facebook 官网 facebook.com 就使用了 React 的并发特性。诸如 「渲染应该是可中断的」,「setState() 应该是并发的」以及「渲染应该在后台完成」这些小小的想法实际上带来了很多的后果,这也是我们花了一段时间才意识到的。

这些特性带来了更好的服务器端渲染(server-rendering),更好的代码分割 (code-splitting) 和更好的数据获取(data-fetching),在将来我们还会用相同的思路去实现更好的动画效果。这个想法在很多地方都得到了体现,因为它本质上为 React 提供了让渲染能够「异步」的能力,这个世界上有很多东西天生就是异步的,比如我上面提到的那些功能。梳理这些功能,搞清楚怎么让他们一起运作起来确实花了我们不少时间。但我们现在基本上都已经搞定了,这些特性都会集成到 React 18 里面。所以,我再一次建议感兴趣的朋友去 React 官网的博客查看 React 18 的发布计划,点击相关的讨论区查看相关的细节。

Dan 在工作组讨论中的解释

Dan Abramov 在 React 18 工作组讨论中是这么解释的:

假设我们需要呼叫两个人:Alice 和 Bob。在非并发机制中,我们一次只能进行一个调用。我们首先呼叫 Alice,结束呼叫,然后呼叫 Bob。

当呼叫时间较短时,这还好,但如果与 Alice 的呼叫等待时间较长,则可能会浪费时间并造成性能卡顿的问题。

非并发呼叫

而在并发机制中,我们可以呼叫 Alice,一旦被搁置,我们就可以去呼叫 Bob。这并不意味着我们同时与两个人交谈。它只是意味着我们可以同时进行两个或多个并发调用,并决定哪个调用更重要(优先级调整)。

并发通话

类似地,在具有并发渲染的 React 18 中,React 可以中断、暂停、恢复或放弃渲染。这使得 React 即使它正在执行繁重的渲染任务时,也能够快速响应用户交互。

现在,解释一下这个比喻,在 React 中,"电话 "就是你的 setState 调用。以前,React一次只能进行一次状态更新。所以造成所有的更新都是 "紧急 "的:一旦你开始重新渲染,你就不能停止。但是有了 startTransition,你可以把一个不紧急的更新标记为一个过渡。

image.png

你可以认为 "紧急 "的 setState 更新类似于紧急电话(例如,你的朋友需要你的帮助),而过渡(transitions)则像轻松的对话,如果不再相关,可以搁置甚至中断。

在 React 18 之前,渲染是一个单一的、不间断的同步事务,一旦渲染开始,就不能被中断。并发性是 React 渲染机制的基础更新。并发允许 React 中断渲染。React 18 引入了并发渲染的基础,以及由并发渲染提供支持的 Suspense、流服务器渲染(streaming server rendering)和过渡(transitions)等新功能。

严格意义上来说,这基本上是协作并发,而不是真正的并发。在简单的并发中,我们会有许多电话一起打开(或者只是一个电话会议?),我们可以同时与任何一个电话互动。由于React使用的JS运行时只是单线程的,这更像是一种协作式的并发,我们可以暂停任务,稍后再返回继续执行。

如果整个React 18的更新必须用一个词来概括,那就是并发。并发渲染将允许React将大型更新分解成较小的块,使浏览器能够更有效地处理更新。在底层来看,并发性基本上意味着任务可以重叠。并且允许我们在多个任务之间来回跳动,而不是一个状态的更新必须完全完成后,系统才能进入下一个任务。

注意:这并不意味着这些事情都是在同一时间发生的,相反,它是指一个任务现在可以暂停,而其他更紧急的任务可以优先执行。

深入理解 “并发” 机制

可以先看看 React 官方 Github repo Discussion 板块关于 CM 的讨论:What happened to concurrent "mode"?

React 的新特性concurrent mode。通过concurrent modes实现了并发的状态更新,这里的并发有两种内含:

  1. 对于CPU相关的更新(如创建DOM节点,执行组件生命周期函数),这意味着高优的更新能够打断正在执行的更新。
  2. 对于IO相关的更新(如请求数据),这意味着在数据返回前可以在内存中提前进行渲染。

在react 18的post中,作者将其重新命名为”concurrent rendering”。意味着react18中并发不再是以一种必须选择的作用于全局的模式(all-or-nothing “mode”),而是只会在需要时被新特性触发的特性。这也和react18的渐进式升级策略相关联。

CPU-bounded updates

为了提升运行性能和用户体验,react修改渲染的调度机制,这主要包含两点:

  1. 将一次更新分片,允许多次更新并发执行,也支持更新的打断
  2. 对更新区分优先级,优先执行高优更新任务

分片

在进入分片之前我们需要先区分一下vdom树的更新(reconciliation)和渲染(render)。更新器(reconciler)是react的核心,对比两次更新前后树结构的不同;而渲染器(renderer)则将更新结果渲染到DOM上。更新器是可插拔的,我们可以用ReactDOM将其渲染为浏览器的DOM,或者用react native将其渲染为客户端的View。而核心的更新器是唯一的。

image-20211204182738628.png

分片能力在react 16引入的 Fiber Reconciler 中就已实现。在此之前react使用的是Stack Reconciler,它使用递归的方式对整棵树进行更新。它的问题在于在触发一次更新后,知道这次更新任务的完成,JS线程会一直被占用而无法执行其他更加高优的任务(如:响应用户交互),造成了用户感知卡顿的问题。而Fiber Reconciler使用while loop拆解递归,将最小执行单元由一整棵树结构的更新降到了一个节点的更新;这就允许了更新被打断,为主线程提供了更多的调度能力。在新的调度能力的支持下,就可以完成高优更新优先渲染、中断过期更新等能力,更好地分配和节省计算资源。

如果你对react的实现细节感兴趣,可以阅读一下codebase overview

优先级

react18将状态更新分为两个类别(优先级):

  1. Urgent updates:需要快速反馈的交互,如:键盘输入、点击、触摸等
  2. Transition updates:UI从一个视图到另一个视图的转换

比如:在一个cms的表格场景中,当用户选中一个下拉框的选项来过滤列表的时候。用户期望在点击选项后下拉框快速收起并更新选中项(urgent updates),但是真实的过滤后的列表无需即时变化(transition updates)。

在18之前,所有的状态更新都是 urgent updates,但实际上很多场景在18中可以归类为transition updates。但为了向后兼容,transition updates被作为一个可选的特性,只有当用户使用对应的API触发状态更新的时候才会被使用。

API

这个新的API就是startTransition

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

// Mark any state updates inside as transitions
startTransition(() => {
  setState(input);
});

useTransition hook 返回了:

  1. isPending:表示当前的状态更新还未被反映(渲染)到视图上

  2. startTransition:用于触发transition updates

    • startTransition会被同步执行,但是这个更新会被标记,在更新被处理时react会以此判断如何渲染更新
    • 渲染时,startTransition造成的更新是可以被打断的,它不会block页面。当用户的输入改变后,react将不会继续渲染过期的更新

下文还有对这两个 API 的详细介绍。

应用场景

在以下的场景中,我们可以用startTransitionAPI来替代之前的状态更新API:

  1. Slow rendering:当一个更新需要消耗大量计算资源的时候
  2. Slow network:当一个更新需要react等待接口返回数据的时候

可以这么认为:当一个更新本身就需要耗费一些等待时间(等待可能是来源于大量计算的开销或网络的等待)时,那么用户不会在乎为这个更新再多等待一些时间,此时就可以使用startTransition API。以此,可以将计算资源更多地提供给urgent updates来提升用户体验。

react18和之前版本的性能比较可以见 这个案例

IO-bounded updates

现状

在目前,有三种渲染异步数据的方式:

  1. Fetch-on-render:在render函数中触发fetch(如:使用useEffect进行fetch),这经常会导致“waterfalls”(即将本可以并行的操作串联导致不必要的等待时间)。
  2. Fetch-then-render:等fetch完成后再进行渲染,但在fetch过程我们无法做任何事。
  3. Render-as-you-fetch:尽早开始fetch,同时开始渲染(在fetch返回之前),fetch返回之后重新进行渲染。

一个例子

在下面的代码中我们可以看到一个简化后的例子,描述了Render-as-you-fetch的流程:

// This is not a Promise. It's a special object from our Suspense integration.
const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
  1. 首先,使用fetchProfileData发送获取数据的请求
  2. 同时,react开始渲染,在渲染ProfileDetailsProfileTimeline时,由于read函数发现数据还没有返回,就会显示最近的祖先Suspense中fallback的内容
  3. 随着数据返回,react将会重新尝试render,层层解锁suspense直到完整渲染

Suspense

上面的例子介绍了Suspense的使用方法。Suspense是react为组件渲染异步获取的数据提供的一个解决方案。

In the long term, we intend Suspense to become the primary way to read asynchronous data from components — no matter where that data is coming from.

在React18之前,Suspense唯一的使用场景就是用以在懒加载React.lazy组件的时候,显示加载中的状态。而Suspense的新功能为请求库提供了一个机制:一个组件渲染所需要的数据是否已经准备好了。它帮助请求库更好地与react进行集成但并非是一个请求库,目前Facebook内部使用的是Relay,而将来我们也将会看到更多请求库支持React Suspense。同时它提供了更友好地展示数据loading状态的方式,但并未将数据获取逻辑和UI组件进行耦合。

优点

  • 分离数据的获取和消费逻辑。在组件中对其依赖的需要消费的数据进行声明,由react自身控制渲染。而开发者可以自由控制数据获取时机(如:在用户点击,页面切换之前就开始请求)。同时请求库的提供者也可以自由控制数据获取逻辑(如:像relay这样batch请求)。

  • 在数据消费处(组件)声明数据依赖。这允许在build阶段进行静态代码分析来进行一些处理(如:relay就以此将数据依赖编译到独立文件中,并且集成GraphQL,以在一次请求中获取这个组件所需的数据)

  • 声明式的加载状态控制。通过Suspense API,可以更加方便地通过标签声明来控制哪些组件需要同时被加载,哪些可以分别展示不同的加载状态。当需求发生变更时也无需侵入性地改变逻辑代码。

  • 避免race condition。在此前,想象在useEffect中触发一个fetch,在then中再setState;如果多次请求,可能会出现老的请求在更晚返回并触发setState。而使用Suspense后,数据获取逻辑本身被作为state传入(类似于promise),这个state本身的产生是同步的,避免了race condition的出现。例子

const initialResource = fetchProfileData(0);  
  
function App() {  
    const [resource, setResource] = useState(initialResource);
    // ...
}
  • 使用ErrorBoundary处理fetch错误。

Break Change

部分生命周期的执行时机和次数将会发生改变。我们可以将react的生命周期分为两类:render phase(渲染阶段)和commit phase(提交阶段),详见react-lifecycle-methods-diagram。在并发渲染时,一个组件的更新可能会被打断,也有可能会被重新恢复,这就导致了一个渲染阶段的生命周期可能会被多次执行,造成切换到并发渲染后组件发生预期外的表现。因此在将旧组件升级为并发渲染时,需要注意:

  1. 将render phase生命周期回调放到commit phase的回调中执行。
  2. 或保证render phase执行的逻辑是幂等的。即该回调中的side effect,多次执行单次执行对系统状态的影响相同

在开发阶段,我们可以:

  1. 通过React的Strict Mode来检测这些潜在的错误(Strict Mode并非直接检测副作用,而是将这些生命周期的回调执行两次以便于用户发现非幂等的副作用)
  2. 不再使用componentWillMount等生命周期,这些生命周期的替代方案可以参考官方文档(这也是为什么React16.9将这些函数命名为UNSAFE_componentWillMount等,并在控制台打印警告)

Server Components

Dan 大神是这么说的

先来看看 2021年 Dan 大神在一次与中国开发者线上交流会中关于 Server Components 的问答吧:

问:Server Component 的 RFC 草案去年年底发布了,请问 Server Component 的主要目的是什么。

Dan:是的,我们去年 12 月发布了一个 Server Component 的技术预览,但它还处于比较早期的阶段。这应该算是一个还处在研究阶段的特性,与 React 18 相比更偏实验性,也不会包含在 React 18 的特性之中,可能会在其之后才发布。但广义上来说,它与 并不一样,这也是人们常常混淆的点。最大的区别是,Server Component 只运行在服务器上,它不会被下载下来。你可以把它们想象成某种 API,以往的客户端应用可能会请求 JSON API 来获取数据, Server Component 与之类似,但是 API 换成了在服务器端运行的组件。这样做的优势是你不需要去下载任何代码,可以达成性能上的优化。另一个优势是,因为这些组件运行在服务器端,它们可以直接与数据库、微服务、或者其他任何在这台服务器上的资源进行通信。你不需要把这些资源暴露给客户端,就把它们留在服务器上就好。

问:有些用户已经尝试过使用 Server Component 了,所以当我们需要在项目中使用服务器组件的时候,我们需要维护三个组件而不是一个,这带来了额外的复杂性不是吗?

Dan:好的,对于这个问题,我个人其实不太认同这种说法。我不想把这个问题定义为我们需要维护三个组件而非一个。我觉得更准确的说法是,在传统的 React 中,我们只有一种类型的组件;而在 Server Component 中,我们把它们分成了 Server Component,Client Component 和 Shared Component。这个问题就好比你原本只有一部手机,现在你既有手机、又有手表、又有电视,但这并不意味着你要同时去用这三个东西,你只是有了更多的选择。所以对于这个问题而言,不是说你的组件在将来都会以三种形态存在,而是你现在只能用 Client Component 的组件。如果只用 Client Component 能够满足需求的话固然很好,但是当这个 Server Component 这个特性落地的时候,你会有更多的选择来做相同的事。到那个时候,如果你想要使用 React 写博客,想要读取文件系统,这些操作不需要很多复杂的框架也能做到。如果你只是想要使用 或者 Rails 类似的传统 web 编程风格去做一些数据库的读取操作,Server Component 也能帮你做到。在大多数场景下,我们预期用户们会通过框架来使用 Server Component 功能,比如使用 Next.js。所以幸运的话,你并不需要想太多,也不需要创建什么 API,你只需要在文件名中加上 .server.js,这个文件就可以在服务器端运行,你就可以使用所有 Server Component 的功能了。

问:所以我们可以直接通过一些框架来使用这个功能?

Dan:是的,我觉得大部分情况应该都是如此。因为框架就像是整个项目的基础设施一样,会为你连接并组织好项目的各个部分。虽然你也可以去手动配置,但是肯定不如使用框架来的方便。特别是框架还可以实现无配置的启用这些功能,如果你要自己动手,你就得自己把项目的各个部分串联起来。目前我们还没有针对如何使用 Server Component 给出使用的建议,但是鉴于 Next.js 已经支持了 Server Component,如果你想要部署自己的 Server Component,你可以去看看他们是怎么实现的,然后复制他们的做法。又或者你可以直接使用 Next.js 或者其他类似的框架。

Dan 大神在GitHub Discussion中的解释

再借用 Dan Abramov 大神在 react github repo 中的 discussion 板块的解释:

服务器组件(Server Components)是一个新的实验性 React 功能。它可能不会成为 React 18 的一部分,但会在之后的某个时间点出现。我们强烈建议你看一下解释这个想法的 演讲,因为没有任何东西像它一样,所以很难将它与现有的东西进行比较。

以下是你可以考虑的方式。你有一个服务器(你的数据库可能在那里)和一个客户端(例如,一个电话或一台电脑)。浏览器在客户端上运行。你的React应用也在客户端上运行。所以你的组件:

<Panel>
  <SearchField />
  <FriendList />
</Panel>

也在客户端(用户的手机或电脑上)运行。这意味着:

  1. 在带有它们的JavaScript <script />标签完成加载之前,它们不会工作。
  2. 如果它们需要从服务器上获取一些数据(比如朋友列表!),你需要为此编写特殊的代码。

服务器组件将允许你把你的组件放在服务器上。例如,如果<Panel><FriendList>组件本身没有事件处理程序(如点击)或状态,它们就没有理由必须在用户的计算机上执行。它们可以在服务器上执行,就像你的API一样。

为什么这样做更好?

  1. 现在你的<script />标签中唯一的组件是SearchField。不需要下载面板或FriendList的代码。
  2. 现在FriendList不需要特殊的代码来从服务器上 "获取 "数据了。它可以直接读取数据,因为它已经在服务器上了。

所以服务器组件是一种方式:

  • 将一些React组件保留在服务器上,这样你就不会将它们的代码发送到电脑上(这可以使你的包更小,你的应用程序更快!)。
  • 简化数据获取(而不是一个组件在效果中 "获取 "它的数据,它可以直接从服务器上读取,因为它在服务器上)。
  • 把服务器和客户端看作是一棵树,而不是两个必须相互 "对话 "的不相干的部分。

Server Component 叫服务端组件,目前还在开发过程中,没有正式发布,不过应该很快就会和我们见面的。

Server Component 的本质就是由服务端生成 React 组件,返回一个 DSL 给客户端,客户端解析 DSL 并渲染该组件。

Server Component 带来的优势有:

  • 零客户端体积,运行在服务端的组件只会返回最终的 DSL 信息,而不包含其他任何依赖。
// NoteWithMarkdown.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

假设我们有一个 markdown 渲染组件,以前我们需要将依赖 markedsanitize-html 打包到 JS 中。如果该组件在服务端运行,则最终返回给客户端的是转换完成的文本。

  • 组件拥有完整的服务端能力。

由于 Server Component 在服务端执行,拥有了完整的 NodeJS 的能力,可以访问任何服务端 API。

// Note.server.js - Server Component
import fs from 'react-fs';

function Note({id}) {
  const note = JSON.parse(fs.readFile(`${id}.json`));
  return <NoteWithMarkdown note={note} />;
}
  • 组件支持实时更新

由于 Server Component 在服务端执行,理论上支持实时更新,类似动态 npm 包,这个还是有比较大的想象空间的。也许 React Component as a service 时代来了。

当然说了这么多好处,Server Component 肯定也是有一些局限性的:

  • 不能有状态,也就是不能使用 state、effect 等,那么更适合用在纯展示的组件,对性能要求较高的一些前台业务
  • 不能访问浏览器的 API。
  • props 必须能被序列化。
  • OffScreen。

OffScreen

OffScreen可以理解为React版的keep-alive

目前也在开发中,会在未来某个版本中发布。但我们非常有必要提前认识下它,因为你现在的代码很可能已经有问题了。

OffScreen 支持只保存组件的状态,而删除组件的 UI 部分。可以很方便的实现预渲染,或者 Keep Alive。比如我们在从 tabA 切换到 tabB,再返回 tabA 时,React 会使用之前保存的状态恢复组件。

为了支持这个能力,React 要求我们的组件对多次安装和销毁具有弹性。那什么样的代码不符合弹性要求呢?其实不符合要求的代码很常见。

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}

在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

警告:不能在已经卸载的组件中更改 state。这是一个无用的操作,它表明你的项目中存在内存泄漏。要解决这个问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

所以我们一般都会通过一个 unmountRef来标记当前组件是否卸载,以避免所谓的「内存泄漏」。

function SomeButton(){
  const [pending, setPending] = useState(false)
  const unmountRef = useUnmountedRef();
  async function handleSubmit() {
    setPending(true)
    await post('/someapi')
    if (!unmountRef.current) {
      setPending(false)
    }
  }
  return (
    <Button onClick={handleSubmit} loading={pending}>
      提交
    </Button>
  )
}

我们来模拟执行一次组件,看看组件的变化状态:

  1. 首次加载时,组件的状态为:pending = false。
  2. 点击按钮后,组件的状态会变为:pending = true。
  3. 假如我们在请求过程中卸载了组件,那此时的状态会变为:pending = true。

在 OffScreen 中,React 会保存住最后的状态,下次会用这些状态重新渲染组件。惨了,此时我们发现重新渲染组件一直在 loading。

怎么解决?解决办法很简单,就是回归最初的代码,删掉 unmountRef的逻辑。至于「内存泄漏」的警告,React 18 删除了,因为这里不存在内存泄漏。

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  setPending(false)
}

为了方便排查这类问题,在 React 18 的 Strict Mode 中,新增了 double effect,在开发模式下,每次组件初始化时,会自动执行一次卸载,重载。

* 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.

这里还是要再提示下:开发环境,在 React 18 的严格模式下,组件初始化的 useEffect 会执行两次,也就是可能 useEffect 里面的请求被执行了两次等。

Suspense

推荐阅读:

简介:

  1. 使用 Suspense 服务端进行代码分割存
  2. 服务器上的流式渲染

怎么理解 Suspense?先来看两个形象的例子:

想象一下,一个组件在渲染之前需要做一些异步任务,比如说获取一些数据。在Suspense之前,这样的组件会保持一个isLoading状态,并在此基础上返回某种回退(空状态、骨架、旋转器...)。有了Suspense,一个组件现在可以在渲染过程中喊出 "HALT!我还没有准备好。请不要继续渲染我--这是一个呼叫器,当我准备好的时候我会呼叫你!React会沿着树走,找到最近的Suspense边界,这个边界是由You设置的,包含了现在要渲染的回退,直到呼叫器响起。

另一种例子是,它就像 try/catch,但对于加载状态。当一个组件说 "我还没准备好"(特殊情况,像扔),上面最接近的暂停块(像抓)就会显示回退的状态。然后,当Promise resolve了,我们就 "重试 "渲染。与传统的基于承诺的编程的一个关键区别是,承诺没有暴露给用户代码。你不会直接处理它们。你不需要Promise.all them或.then them或类似的东西。与其说是 "等待Promise解决并运行一些用户代码",不如说是 "React 会在它 resolve 时重试渲染"。在某些方面,这更简单,但它需要一些 "放松学习",因为用户失去了一些他们可能习惯于从传统的 Promise-base 编程中得到的控制。

客户端渲染(CSR)与服务器渲染(SSR)

在客户端渲染的应用程序中,你从服务器上加载页面的HTML以及运行页面所需的所有css、JavaScript,并使其具有交互性。

然而,如果你的 css、JavaScript包 如果很大,或者你的连接速度很慢,这个过程可能需要很长的时间,用户将等待页面变得互动,或者看到有意义的内容。

image.png

说明:在客户端渲染流程中,用户需要等待很长时间才能使页面变得互动。资料来源:《React Conf 2021》:React Conf 2021 流媒体服务器渲染的 Suspense

为了优化用户体验,避免用户不得不坐在一个空白的屏幕前,我们可以使用服务器渲染。

服务器渲染是一种技术,你在服务器上渲染React组件的HTML输出,并从服务器上发送HTML。这可以让用户在JS包加载时和应用程序变得具有互动性之前查看一些UI。

关于客户端与服务器渲染的详细概述,请查看Shaundai Person的 React Conf 2021演讲

image.png

在服务器渲染流程中,我们可以通过从服务器发送HTML来更快地向用户显示有意义的数据。资料来源:《React Conf 2021》:React Conf 2021 流媒体服务器渲染的 Suspense

服务器渲染进一步增强了加载页面的用户体验,减少了互动时间。

现在,如果你的应用程序的大部分都是快速的,除了一个部分呢?也许这部分加载数据很慢,或者需要下载大量的JS才能实现交互。

在React 18之前,这部分往往是应用的瓶颈,会增加渲染组件的时间。

一个慢的组件会拖累整个页面。这是因为服务器渲染是全有或全无的--你不能告诉React推迟加载一个慢的组件,也不能告诉React为其他组件发送HTML。

React 18在服务器上增加了对Suspense的支持。在Suspense的帮助下,你可以将应用的慢速部分包裹在Suspense组件内,告诉React推迟慢速组件的加载。这也可以用来指定一个加载状态,可以在加载时显示。

在React 18中,一个慢速组件不必拖累你整个应用的渲染。有了Suspense,你可以告诉React在发送占位符的HTML的同时,先发送其他组件的HTML,比如加载旋转器。然后,当慢速组件准备好并获取其数据后,服务器渲染器将在同一流中弹出其HTML。

image.png

图像显示,服务器上的悬Suspense可以让一个缓慢的组件显示加载状态,而其他组件则完全渲染。

这样,用户可以尽早看到页面的骨架,并看到它随着更多的HTML片段的出现而逐渐显示出更多的内容。

所有这些都发生在任何JS或React加载到页面之前,这大大改善了用户体验和用户感知的延迟。

推荐阅读:

流式 SSR(Stream SSR)

大多数人认为React是在客户端运行的(即在用户的浏览器中)。当组件被渲染时,会导致改变页面上的HTML。当用户使用React访问网页时,首先我们需要下载React和每个React组件的JavaScript代码,然后在他们的浏览器中运行这些代码,找出我们想在屏幕上显示的HTML。一旦发生这种情况,用户就可以看到应用程序并与之互动。

服务器端渲染(SSR)是一种工具,旨在加速这一过程,以便使用React的页面加载更快。当使用SSR时,我们首先在服务器上运行组件,将页面的文件发送给用户。我们将这些组件输出的HTML发送到用户的浏览器上。当它到达时,用户就可以看到页面的全部内容。请注意,HTML本身只是页面最初应该有的一个快照,它本身并不具有交互性。因此,我们仍然需要下载React和每个React组件的JS代码,然后我们需要告诉React来 "控制 "已经在页面中的HTML,这就是所谓的水化。一旦完成,用户就可以看到页面的全部内容,他们可以与之互动。

SSR有助于使初始页面更快可见,但它并不能使页面更快交互,因为你仍然需要等待下载和运行所有相同的JS代码--使用SSR,我们现在在服务器上运行所有的组件,但我们也仍然在客户端运行所有的组件。(即将进行的关于服务器组件(server components)的工作——React团队的新实验,将有助于使你可以只在服务器上运行一些组件,这可以帮助解决这个问题,但它在一段时间内不会准备好。)

为了使用SSR,你需要做两件事:

  1. 确保你代码库中的每个React组件只使用在服务器和客户端都能工作的功能(有一些要求,比如不使用只在浏览器中可用的window对象);
  2. 改变你的服务器端代码,以调用React并对产生的HTML做一些处理。一些框架,如Next.js,开箱即有SSR支持,意味着他们已经为你完成了第二件事;

SSR 一次页面渲染的流程大概为:

  1. 服务器 fetch 页面所需数据。
  2. 数据准备好之后,将组件渲染成 string 形式作为 response 返回。
  3. 客户端加载资源。
  4. 客户端合成(hydrate)最终的页面内容。

在传统的 SSR 模式中,上述流程是串行执行的,如果其中有一步比较慢,都会影响整体的渲染速度。而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。

假设我们有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>

image.png

但如果 Comments 数据请求很慢,会拖慢整个流程。在 React 18 中,我们通过 Suspense包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

如上,我们通过 Suspense包裹了 Comments 组件,那服务器首次返回的 HTML 是下面这样的,<Comments />组件处通过 loading 进行了占位。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

image.png

<Comments /> 组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

更多关于流式 SSR 的讲解可见:传送门

批处理(Batching)和自动批处理(Automatic Batching)

React 18 引入了一个新功能,可以自动批处理多个状态更新,减少组件重新渲染的次数。批处理是指 React 将多个状态更新分组到一次重新渲染中以获得更好的性能。这对性能来说是很好的,因为它避免了不必要的重新渲染。

如果没有自动批处理,我们只在React事件处理程序中进行批处理更新。默认情况下,React不会对PromisessetTimeout原生事件或任何其他事件中的更新进行批处理。有了自动批处理,这些更新将被自动批处理。

// Before: 只有React事件被分批处理。
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React会渲染两次,每次状态更新一次(没有批处理)。
}, 1000);
// After: 在超时内的更新,Promise、本地事件处理程序或任何其他事件都是分批进行的。
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React只会在最后重新渲染一次(这就是批处理!)。
}, 1000);

下面引用 Dan Abramov 的解释。

批处理(Batching)

想象一下,你正在做早餐。你去市场买些牛奶。你回来之后。然后你意识到你需要燕麦片。你去市场买燕麦片。然后你回来以后。然后你才意识到你还需要饼干。你去市场买了饼干。终于,你可以吃你的早餐了。

但肯定有一个更有效的方法来做到这一点?与其一想到就去买每样东西,不如先编一个购物清单。然后你去市场,买到你需要的所有东西,然后做你的早餐。这就是 批处理

在React的例子中,每个单独的项目都是一个 setState 的调用:

setIsFetching(false);
setError(null);
setFormStatus('success')

React可以在每次调用后重新渲染。但这并不高效。最好的办法是先记录所有的状态更新。然后,当你完成所有的状态更新后,重新渲染一次。但是,挑战在于React应该什么时候做这件事。它怎么能知道你已经完成了状态的更新?

对于事件处理程序来说,这很容易:

function handleClick() {
  setIsFetching(false);
  setError(null);
  setFormStatus('success')
}

React 是调用你的点击处理程序的东西:

// Inside React
handleClick()

所以 React 将在之后立即重新渲染:

// Inside React
handleClick()
updateScreen();

React 一直都是这样工作的。

自动批处理(Automatic Batching)

回到这个例子:

setIsFetching(false);
setError(null);
setFormStatus('success')

如果不在一个事件中发生呢?例如,也许这段代码是作为一个获取的结果而运行的。React不知道你何时会 "停止"设置状态。所以它需要挑选一些时间来更新屏幕。这就类似于也许你没有吃早餐,只是在一天中随机的渴望。你是在觉得自己饿的那一刻就去市场买东西呢?还是等待一段时间(比如30分钟),然后带着你目前想出来的清单去市场?

以前,React在这种情况下会立即重新渲染。这意味着你会因为三次 setState 调用而造成三次屏幕更新,这不是很有效率。有了React 18的自动批处理功能,它将总是批处理它们。实际上,这意味着React会 "等待一下"(技术术语是 "直到任务或微任务结束")。然后React会更新屏幕。

深入理解批处理

为了深入理解批处理,让我们再来看同React工作组讨论中的杂货店购物的例子。

比方说,你正在为晚餐做意大利面。如果你要优化你的杂货店之旅,你会创建一个你需要购买的所有材料的清单,去杂货店一趟,并在一次购物中获得所有的材料。

这就是批量化。如果没有分批,你就会开始做饭,发现你需要一种原料,去杂货店买原料,回来后继续做饭,却发现你需要另一种原料,再去杂货店......把自己逼疯。

在React中,当你调用setState时,批处理有助于减少状态变化时发生的重新提交的次数。以前,React在事件处理程序中批处理状态更新,例如:

const handleClick = () => {
    setCounter();
    setActive();
    setValue();
}

//re-rendered once at the end.

然而,在事件处理程序之外发生的状态更新是不分批的。例如,如果你有一个承诺或正在进行一个网络调用,状态更新就不会被分批处理。像这样:

fetch('/api/...').then( () => {
    setCounter(); //re-rendered 1 times
    setActive();  //re-rendered 2 times
    setValue();   //re-rendered 3 times
});

// Total 3 re-renders

正如你所知道的,这是不可能实现的。React 18引入了自动批处理,允许所有的状态更新——甚至在 PromisessetTimeouts事件回调中一一都会被批处理。这大大减少了 React 在后台需要做的工作。React 会等待一个微任务完成后再重新渲染。

flushSync: 停止自动批处理

现在我们已经了解了自动批处理的强大功能,自动批处理在React中是开箱即用的,但有时我们可能并不需要此功能。但如果你想选择退出,你可以使用 flushSync

flushSync 是一个方法的名字,它可以控制React何时更新屏幕("冲刷" 状态更新)。特别是,它可以让你告诉React现在就这样做("同步"):

flushSync(() => {
  setState(something);
});
// 通过这一行,屏幕已被更新

"Flush synchronously" = "Update the screen now"

通常情况下,你可能不需要 flushSync。只有当你需要解决React更新屏幕的时间晚于你需要的问题时,你才会使用它。在实践中,它很少被使用。

React 为我们提供了一种通过 flushSync() 退出自动批处理的方法。让我们看一下下面的例子:

首先我们从 react-dom 导入flushSync()

import { flushSync } from 'react-dom';

接下来我们定义事件处理程序,然后将状态更新放置在 flushSync() 中:

const handleTimeOutClick = () => {
    flushSync(() => {
        setCount(count + 1); // re-render occurs here
    });
    setCount2(count2 / 2); // re-render occurs here
};

React将首先在 flushSync() 中更新DOM ,然后在 setCount 中再次更新DOM 。

过渡(transitions)和 startTransition

React 在 React 18 中引入了过渡,以便开发人员能够确定更新的优先级。现在,开发人员可以区分紧急更新和非紧急更新。

紧急更新(例如键入或单击)需要立即响应。否则他们会感觉不自然,因为我们期望按钮在单击时做出响应。过渡和非紧急更新处理 UI 从一个视图到另一个视图的更改,因为用户不希望立即在屏幕上看到值。

过渡可以用来标记不需要紧急资源更新的用户界面更新。

例如,当在typeahead字段中输入时,有两件事情发生:一个闪烁的光标显示你正在输入的内容的视觉反馈,和一个在后台搜索输入的数据的搜索功能。

向用户显示视觉反馈是很重要的,因此是很紧急的。搜索则不那么紧急,因此可以被标记为非紧急。

这些非紧急的更新被称为过渡期。通过将非紧急的UI更新标记为 "过渡",React将知道哪些更新需要优先处理。这使得优化渲染和摆脱陈旧的渲染变得更加容易。

你可以通过使用startTransition将更新标记为非紧急状态。下面是一个例子,说明一个typeahead组件被标记为过渡时的情况:

import { startTransition } from 'react';

// 紧急: 显示什么被输入了
setInputValue(input);

// 将里面任何非紧急的状态更新标记为过渡
startTransition(() => {
  // 过渡:显示结果
  setSearchQuery(input);
});

transitions 与 debouncing 或 setTimeout 有什么不同?

  • startTransition是立即执行的,与setTimeout不同。
  • setTimeout有一个保证的延迟,而startTransition的延迟取决于设备的速度,以及其他紧急渲染的情况。
  • startTransition的更新可以被打断,不像setTimeout那样,不会冻结页面。
  • 当用startTransition标记时,React可以为你跟踪待定状态。

startTransitions

推荐阅读:

我们如果要主动发挥 CM 的优势,那就离不开 startTransition。

React 的状态更新可以分为两类:

  • 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡,或者出问题了的感觉
  • 过渡更新(Transition updates):将 UI 从一个视图过渡到另一个视图。不需要即时响应,有些延迟是可以接受的。

我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。

这是因为 React 并不能自动识别哪些更新是优先级更高的。

const [inputValue, setInputValue] = useState();
const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  setSearchQuery(e.target.value);
}
return (
  <input value={inputValue} onChange={onChange} />
)

比如以上示例,用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。

但是 React 确实没有能力自动识别。所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。

// 紧急的
setInputValue(e.target.value);
startTransition(() => {
  setSearchQuery(input); // 非紧急的
});

如上代码,我们通过 startTransition来标记一个非紧急更新,让该状态触发的变更变成低优先级的。

  • 社区有一个 demo 来认识下可中断渲染对性能的爆炸提升。这个 demo 展示了当每一个状态变化都会更新 1,000,000 多个节点时会发生什么。左边的滑块使树增长,使问题恶化-——呈指数增长。顶部的滑块使树倾斜,更新每个节点。用它来看看会发生什么。

  • 还找到一个:Real world example: adding startTransition for slow renders

水合(Hydration)

推荐阅读:

Dan 大神的解释:

The process of rendering your components and attaching event handlers is known as “hydration”. It’s like watering the “dry” HTML with the “water” of interactivity and event handlers.

渲染组件和附加事件处理程序的过程称为“水合”。这就像用交互性和事件处理程序的“水”来浇灌“干”的 HTML。

需要知道的是,水合(Hydration) 只用于服务器端的渲染(SSR)。当你使用SSR时,在浏览器中渲染组件的过程有两个步骤:

  1. 将非交互式的、服务器端渲染的HTML和CSS发送到浏览器。这样,你的用户在等待你的页面的JS加载时就有东西可看了。
  2. 当JS加载完毕后,"水合" HTML和CSS,这样用户就可以与页面上的东西互动(比如:点击按钮、切换下拉菜单等)。

Prioritized Hydration

考虑一个更复杂的页面,其中有两个部分,由于存在大量交互元素,加载时间较长。例如,网站标题、侧边栏和两个内容组件都是水合的。如果用户想在 UI 仍在加载时与其进行交互,React 18 的并发渲染引擎将优先考虑用户点击的组件的水合作用。这意味着组件树比其他组件更快地变得更具交互性。

使用 React 18 的并发渲染引擎优先考虑水合作用。

并发渲染引擎在内部处理组件的这种选择性水合作用,从而提供更无缝的用户体验。通过单击页面的特定区域,水合过程会被中断并重新安排,从而确保更快的加载时间和更快速的交互 UI。作为开发人员,只要您使用新的 Hydration API ,您就不必担心这种情况是如何发生的。

React 将 SSR 与 <Suspense> 组件以及客户端新的可中断水合功能相结合,提供了明显的优势,可以极大地增强用户体验,并有助于用户在与网页交互时获得整体积极的印象。此外,这些好处还伴随着保持良好搜索引擎优化的额外好处,使其成为“双赢”的局面。

Strict mode

在未来,我们希望增加一个功能,允许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.

React 18中的严格模式将模拟挂载、卸载,并以之前的状态重新挂载组件。这为将来的可重用状态奠定了基础,React可以通过使用卸载前的相同组件状态重新挂载树,立即挂载之前的屏幕。严格模式将确保组件对被挂载和卸载多次的效果具有弹性。请看这里的确保可重用状态的文档

新 React Hooks

useId

useId是一个新的钩子,用于在客户端和服务器上生成唯一的ID,同时避免了水化不匹配。它主要适用于与需要唯一ID的可访问性API集成的组件库。这解决了一个在React 17及以下版本中已经存在的问题,但在React 18中更加重要,因为新的流媒体服务器渲染器是如何不按顺序地提供HTML的。

const id = useId();

使用:

const Check= () => {
    const id = useId();
    
    return (
        <div>
            <label htmlFor={id}>is this appealing to you?</label>
            <input id={id} type="checkbox"/>
        </div>
    );
};

支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容。原理是每个 id 代表该组件在组件树中的层级结构。

注意: useId不是用来生成列表中的 key 的。密钥应该从你的数据中生成。

useTransition

useTransitionstartTransition 让你把一些状态更新标记为不紧急。其他状态更新在默认情况下被认为是紧急的。React将允许紧急的状态更新(例如,更新一个文本输入)打断非紧急的状态更新(例如,渲染一个搜索结果列表)。

function App() {
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        startTransition(() => {
            // set a state
        })
    }

    return (
        <div>
            {isPending && <Spinner />} // renders a spinner while still pending
        </div>
    );
}

useDeferredValue

useDeferredValue 让你推迟重新渲染树的一个非紧急部分。它类似于debouncing,但与之相比有一些优势。没有固定的时间延迟,所以React会在第一次渲染反映在屏幕上后立即尝试延迟渲染。延迟渲染是可中断的,不会阻止用户输入。

const deferredValue = useDeferredValue(value);

useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValuestartTransition 一样,都是标记了一次非紧急更新。

如果你想对一个没有在调用 useTransition() 的组件内更新的更新应用过渡,你可以使用useDeferredValue()。在下面的例子中,查询变量是在组件外更新的(自定义钩子);如果你想把查询更新当作一个过渡,把它传递给 useDeferredValue()。使用查询变量的SearchInput元素的更新是即时的,过渡被应用到查询变量、使用useDeferredValue()创建的deferredQuery变量的SearchSuggestions元素的更新将在幕后由一个过渡执行。

function Typeahead() {
  const query = useSearchQuery('');

  const deferredQuery = useDeferredValue(query);

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">
        <SearchSuggestions query={deferredQuery} />
      </Suspense>
    </>
  );
}
// https://github.com/reactwg/react-18/discussions/129

New in 18: useDeferredValue · reactwg/react-18 · Discussion #129

可以这样:

const [treeLeanInput, setTreeLeanInput] = useState(0);
const deferredValue = useDeferredValue(treeLeanInput);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={deferredValue} />
  </>
)

useSyncExternalStore

useSyncExternalStore是一个新的钩子,它允许外部存储支持并发读取,强制更新到存储是同步的。在实现对外部数据源的订阅时,它消除了对useEffect的需求,并推荐给任何与React外部状态集成的库。

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useSyncExternalStore 能够让 React 组件在 Concurrent Mode 下安全地有效地读取外接数据源。在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。useSyncExternalStore 一般是三方状态管理库使用,一般我们不需要关注。

注意: useSyncExternalStore旨在被库使用,而不是应用程序代码。

useInsertionEffect

useInsertionEffect 是一个新的钩子,允许CSS-in-JS库解决在渲染中注入样式的性能问题。除非你已经建立了一个CSS-in-JS库,否则我们不指望你会使用这个。这个钩子将在DOM被突变后运行,但在布局效果读取新的布局之前。这解决了一个在React17及以下版本中已经存在的问题,但在React18中更为重要,因为React在并发渲染时向浏览器让步,给它一个重新计算布局的机会。

useInsertionEffect(didUpdate);

这个 Hooks 只建议 css-in-js库来使用。这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 生效之前,一般用于提前注入<style>脚本。

注意: useInsertionEffect旨在被库使用,而不是应用程序代码。详细的信息,你可以访问 官方文档

如何升级到 React 18

官方:如何升级到 React 18

使用 npmpnpmyarn 安装 React 18React DOM

npm install react react-dom
// yarn add react react-dom
// pnpm add react react-dom

然后,你需要使用createRoot 而不是之前的 render了。在 index.js 中,更新 ReactDOM.renderReactDOM.createRoot 创建根目录,并使用根目录渲染您的应用程序。

在 React 18 之前它是这样的:

import ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

ReactDOM.render(<App />, container);

这是 React 18 中的样子:

import ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// create a root
const root = ReactDOM.createRoot(container);

//render app to root
root.render(<App />);

参考和推荐阅读资源

end ~