React 19 新特性集合

前言:https://juejin.cn/post/7337207433868197915

新 React 版本信息

伴随 React v19 Beta 的发布,React v18.3 也一并发布。

React v18.3相比最后一个 React v18 的版本 v18.2 ,v18.3 添加了一些警告提示,便于尽早发现问题,从而在升级 React v19 时更容易。

React 18.3 更新内容

React 18.3 相对于 18.2 增加了对废弃 API 的警告以及其他为 React 19 所需的更改。

  1. React
  • 允许向 this.refs 写入以支持字符串 ref 的代码模式转换。
  • 在 StrictMode 外部使用已废弃的 findDOMNode 时,将发出警告。
  • 对使用已废弃的测试工具方法时发出警告。
  • 在 StrictMode 外部使用已废弃的遗留 Context 时,将发出警告。
  • 在 StrictMode 外部使用已废弃的字符串 ref 时,将发出警告。
  • 对函数组件中使用已废弃的 defaultProps 发出警告。
  • 当在组件或元素中展开 key 时,将发出警告。
  • 从测试工具中使用 act 时,如果方式不当,将发出警告。
  1. React DOM
  2. 对使用已废弃的 unmountComponentAtNode 方法时发出警告。
  3. 对使用已废弃的 renderToStaticNodeStream 方法时发出警告。

React v18 发布后,带来了以并发特性为主的各种新 API ( startTransition / useDeferredValue 等 )、新运作模式、及 stream SSR 上的改进等,其相比 React v17 像是一个增量的升级版。

而 React v19 则不然,包含了 大量 细小的、或破坏性的变更(如document meta data 和asset loading)。

由于 React v18 canary 已迭代很久,本次更新中的很多内容属于历史已公布的内容,故不会重复做展开介绍。

React v19 中的新特性 -- 概述

以下是 React 19 将具有的新特性的快速概述:

🤖 React 编译器react compiler:React 正在努力实现一个新的编译器。目前,Instagram 已经在利用这项技术,它将在未来版本的 React 中发布。

🔥 服务器组件react server component:经过多年的开发,React 引入了服务器组件的概念。您现在可以在 Next.js 中使用此功能。

💪 动作action:动作还将彻底改变我们与 DOM 元素的交互方式。

🌇 文档元数据document metadata:另一个急需改进的方面即将到来,使开发人员能够用更少的代码实现更多功能。

💼 资源加载asset loading:这将使资源能够在后台加载,从而改善应用程序的加载时间和用户体验。

⚙️ Web 组件:这特别令人着迷:React 代码现在将使我们能够集成 Web 组件。我对此发展非常激动,因为它将开启无数可能性。

🪝 增强型钩子:令人兴奋的新钩子即将问世,将彻底改变我们的编码体验。

  • 新的 use() 钩子
  • useFormStatus() 钩子
  • useFormState() 钩子
  • useOptimistic() 钩子

React 19 将解决 React 长期存在的挑战之一:过度重新渲染的问题。开发人员历来花费了大量时间来解决这个问题,这经常导致性能问题。

对导致重新渲染的代码进行持续追踪和优化工作一直是工程师们的重复任务。但是有了 React 19,这个问题将得到缓解。框架将自动处理重新渲染,简化开发流程。

以前,开发人员依赖 useMemo()、useCallback()、memo 等技术来管理重新渲染。但是在 React 19 中,这样的手动干预将不再必要。

框架将在幕后智能识别和记忆代码,从而产生更清晰和更有效的代码。

React v20

目前已知内容:

  • React compiler (编译优化器,如 React forget )不等于 React v19 。
  • Activity ( 原 Offscreen )可能在 React v20 推出。

V19新特性介绍

1. React 编译器

这是为了性能做的更新。优化重新渲染的一种方式是手动使用 useMemo()、useCallback() 和 memo API。根据 React 团队的说法,这是一种"合理的手动妥协"。他们的愿景是让 React 管理这些重新渲染。但是 React 团队意识到手动优化很繁琐,而社区的反馈鼓励他们解决这个问题。因此,React 团队创建了"React 编译器"。React 编译器现在将管理这些重新渲染。React 将自动决定何时以及如何更改状态并更新 UI。

React Compiler ,在开发者不调整任何代码的情况下,自动优化项目性能。帮助我们在不使用 memo/useMemo/useCallback 的情况下,方便的处理好项目 re-render 的问题。你的项目最终只会在需要更新的地方 re-render。React Compiler 编译之后,你的代码并不会改变现有渲染机制,而是在已有机制下完成对项目的优化。

React Compiler能够将React代码编译成更为优化的JavaScript代码。这一改变能够显著提升React应用的运行效率。编译器的引入不仅优化了代码的执行时间,还减少了浏览器的负载,从而使应用响应更快,交互更流畅。

与依赖追踪的细粒度更新不同,React Compiler 通过记忆的方式,让组件更新只发生在需要更新的组件,从而减少大量 re-render 的组件。

但是请注意,React Compiler 并非全能,如果你写的代码过于灵活,无法被提前预判执行行为,那么 React Compiler 将会跳过这一部分的优化。因此好的方式是在项目中引入严格模式,在严格模式的指导下完成的开发,基本都在 React Compiler 的辐射范围之内

例子1:在 React19 之后,不再需要使用 useMemo() 钩子,因为 React 编译器将自行进行记忆。

之前:

javascript 复制代码
import React, { useState, useMemo } from 'react';

function ExampleComponent() {
  const [inputValue, setInputValue] = useState('');

  // 缓存检查输入值是否为空的结果
  const isInputEmpty = useMemo(() => {
    console.log('检查输入是否为空...');
    return inputValue.trim() === '';
  }, [inputValue]);

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入一些内容..."
      />
      <p>{isInputEmpty ? '输入为空' : '输入不为空'}</p>
    </div>
  );
}

export default ExampleComponent;
之后:不需要再使用 useMemo 缓存值了 - React19 将在底层自行处理。
javascript 复制代码
import React, { useState } from 'react';

function ExampleComponent() {
  const [inputValue, setInputValue] = useState('');

  const isInputEmpty = () => {
    console.log('检查输入是否为空...');
    return inputValue.trim() === '';
  });

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入一些内容..."
      />
      <p>{isInputEmpty ? '输入为空' : '输入不为空'}</p>
    </div>
  );
}

export default ExampleComponent;

2. 服务器组件

React Server Components 是 React 在探索前端边界的又一次突破性的创举。它是一种新概念组件。我们可以在构建时运行一次组件,以提高页面的渲染速度。

预渲染、增量渲染、流式传输等概念对提高大型复杂项目的用户体验有非常大的帮助。好消息是,RSC 已经在 Next.js 中得到落地实践。

React 19加强了对服务端组件(Server Components)的支持,这些组件在服务器上渲染完成后再发送到客户端,极大地提高了页面加载速度。

这一特性尤其适合内容重的网站应用,允许部分组件的逻辑在服务器上执行,而不会发送相关的JavaScript到客户端。这样做的好处包括减少了传输数据量,加快了首次加载速度,并优化了SEO。

举个例子:

javascript 复制代码
// ServerUserInfo.react.server.js
// 注意:这个文件的扩展名为 .server.js,表示它是一个服务端组件。

import { db } from './database';

// 从数据库中获取用户信息的函数
async function fetchUserData(userId) {
  return db.query('SELECT * FROM users WHERE id = $1', [userId]);
}

function ServerUserInfo({ userId }) {
  const userData = fetchUserData(userId);
  return (
    <div>
      <h1>User Information</h1>
      <p>Name: {userData.name}</p>
      <p>Email: {userData.email}</p>
    </div>
  );
}

export default ServerUserInfo;

这样我们就创建了一个服务端组件,它直接从数据库获取用户信息,并在服务器上进行渲染。这样做还可以保护用户的敏感信息,因为这些数据不会被发送到客户端,同时增强了安全性,并且由于减少了客户端处理逻辑,页面加载速度更快。

在客户端中,我们只需要引用这个组件,并在构建时配置好相关的服务端渲染逻辑。

服务端组件带来的额优势:

  • SEO:服务器渲染的组件通过向网络爬虫提供更可访问的内容来增强搜索引擎优化。
  • 性能提升:服务器组件有助于更快地加载页面和改善整体性能,特别是对于内容密集型应用程序。
  • 服务器端执行:服务器组件使在服务器上执行代码变得无缝和高效,使诸如 API 调用之类的任务变得轻松。

目前 Next.js 支持服务器端组件。React 中所有组件默认都是客户端的。只有当你使用 'use server' 时,组件才是服务器组件

可以在同一项目中的任何 React 组件中导入 requestUsername。在导入服务器组件后,我们可以使用"Actions"(我们很快会学习到)执行特定任务。

javascript 复制代码
'use server';

export default async function requestUsername(formData) {
  const username = formData.get('username');
  if (canRequest(username)) {
    // ...
    return 'successful';
  }
  return 'failed';
}

3. 动作 action

submitData 是服务器组件中的动作。form 是一个客户端组件,它使用 submitData 作为动作。submitData 将在服务器上执行。客户端(form)和服务器(submitData)组件之间的通信只有通过动作属性才能实现。

javascript 复制代码
"use server"

const submitData = async (userData) => {
    const newUser = {
        username: userData.get('username'),
        email: userData.get('email')
    }
    console.log(newUser)
}
const Form = () => {
    return <form action={submitData}>
        <div>
            <label>Name</label>
            <input type="text" name='username'/>
        </div>
        <div>
            <label>Name</label>
            <input type="text" name="email" />
        </div>
       
 <button type='submit'>Submit</button>
    </form>
}

export default Form;

相关新增的api:

新 API :useActionState

重要性:★

参考信息:useActionState 代码例子

此 API 原名为 useFormState ,主要用于联动原生 <form /> 表单提交,现代开发更多使用高级表单库,故此特性不重要。

新 API :useFormStatus

重要性:★

参考信息:useFormStatus 文档

此 API 与 useFormState 联动使用,现代开发更多使用高级表单库,故此特性不重要。

新 API :useOptimistic

重要性:★★

参考信息:useOptimistic 文档

通过此 API 派生其他的 state ,从而在提交动作时,立即乐观更新此值来优化用户视觉上的交互体验。

要实现乐观更新,必然要编写许多额外逻辑,同时现代请求库(如 react-querySWR 等)早已提供了乐观更新的功能。然而更多时候,我们没有精力和工时来提升用户体验,过度提升体验反而会造成更多冗余的乐观更新逻辑导致后续维护困难,展示 loading 已经足够,故此 API 不重要,若不是有心为之,则很难用到。

4. Web 组件

Web 组件允许你使用原生 HTML、CSS 和 JavaScript 创建自定义组件,并将它们无缝地整合到你的 Web 应用程序中,就像它们是标准 HTML 标签一样。

之前,在 React 中集成 Web 组件并不简单。通常情况下,要么需要将 Web 组件转换为 React 组件,要么安装额外的包并编写额外的代码,以使 Web 组件与 React 协同工作。

React 19改进了对Web组件的支持,使得在React项目中整合使用Web标准组件变得更加无缝。这使得开发者可以利用广泛的Web组件生态系统,而不必为了在React中使用它们而写大量的封装代码。

javascript 复制代码
import React from 'react';

function CustomButtonComponent() {
  return (
    <>
      <my-button>Click Me!</my-button>
    </>
  );
}

export default CustomButtonComponent;

在这个示例中,<my-button>是一个自定义的Web组件,它可以直接在React中使用,无需任何特别的封装或桥接代码。这种整合提高了代码的复用性,并允许React开发者利用现有的Web组件库。

可能有的同学会说,这一个自定义的标签有什么用呢?回答是:非常有用。

因为在React 19的现代Web框架中使用Web组件时,<my-button>并不是一个普通的HTML标签。

它实际上是一个Web组件------这是一种使用自定义元素、Shadow DOM、HTML模板 封装好的可重用组件。 当你在Web应用中使用像<my-button>这样的自定义元素时,浏览器会渲染出这个元素的定义所包含的所有样式和行为。

这些自定义元素的行为和外观完全由你定义的Web组件的JavaScript和CSS控制。以下假设定义了一个自定义的web组件:

javascript 复制代码
class MyButton extends HTMLElement {
  constructor() {
    super(); // 调用父类的constructor
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>
        button {
          background-color: blue;
          color: white;
          padding: 10px 20px;
          border: none;
          border-radius: 5px;
          cursor: pointer;
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}

// 定义新的自定义元素
customElements.define('my-button', MyButton);

5. 文档元数据

对元数据的管理变得更加简单。React 19引入了可以直接在React组件中使用HTML元数据标签的能力,这样开发者就不再需要依赖如react-helmet这样的库来动态更新文档头部信息。

1)基本元数据标签

考虑到 SSR 的完备性,使用第三方库插入元信息仍然更好,所以此特性不重要。

之前:

javascript 复制代码
import React, { useEffect } from 'react';

const HeadDocument = ({ title }) => {
  useEffect(() => {
    document.title = title;

 	const metaDescriptionTag = document.querySelector('meta[name="description"]');
    if (metaDescriptionTag) {
    metaDescriptionTag.setAttribute('content', 'New description');
    }
  }, [title]);

  return null;
};

export default HeadDocument;

现在:在 React 中以前是不可能的。唯一的方法是使用像 react-helmet 这样的包。

javascript 复制代码
function HomePage() {
  return (
    <>
      <title>主页 - 我的网站</title>
      <meta name="description" content="这是我的网站主页,提供各种服务信息。" />
      <h1>欢迎来到我的网站</h1>
      <p>这里有丰富的资源和信息。</p>
    </>
  );
}

在这个示例中,<title><meta>标签直接放置在组件返回的JSX中。

React 19可以处理这些标签,并将它们正确地渲染到HTML文档的头部。这种方式简化了元数据的管理,特别是在单页面应用中,可以更灵活地针对不同页面设置SEO相关的元数据。

2)样式表支持

参考信息:插入 link / meta / script / style / title 文档说明

对于使用 webpack 等打包工具进行模块化开发的现代,除 特别的样式覆盖 或 异步分包、构建工具缺陷 等原因导致对顺序出现 workaround 的需求外,一般我们在业务项目内不会接触到手动管理标签的情况,故此特性不重要

在Web开发中,样式表的管理至关重要,无论是通过外部链接(<link rel="stylesheet" href="...">)还是内嵌方式(<style>...</style>)引入,都需要在 DOM 中精准布局,以确保样式优先级得到妥善处理。然而,构建一个能够支持组件内部样式表组合的机制往往十分繁琐,因此开发者常常面临权衡:要么将样式远离其依赖的组件加载,牺牲组织性;要么依赖外部样式库,增加额外的复杂性。

React 19 针对这一挑战提供了内置支持,不仅简化了样式表的管理流程,还进一步增强了与客户端并发渲染和服务器端流式渲染的集成能力。通过指定样式表的优先级,React 将自动处理样式表在DOM中的插入顺序,确保在展示依赖于特定样式规则的内容之前,相关的样式表(无论外部还是内嵌)已经被加载并应用。

javascript 复制代码
function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- will be inserted between foo & bar
    </div>
  )
}

在服务端渲染过程中,React会将样式表包含在<head>标签中,以确保浏览器在加载完成前不会进行页面绘制。如果在已经开始流式传输后才发现样式表,React 将确保在客户端将样式表插入到<head>标签中,然后再展示依赖于该样式表的Suspense边界的内容。

在客户端渲染过程中,React会等待新渲染的样式表加载完成后再提交渲染结果。如果在应用中的多个位置渲染了这个组件,React将确保样式表在文档中只被包含一次。

javascript 复制代码
function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // 不会导致 DOM 中出现重复的样式表链接
  </>
}

对于那些习惯于手动加载样式表的开发者来说,React 19 的这一改进为他们提供了一个便利的机会。现在,可以将样式表直接放在依赖它们的组件旁边,这不仅有助于提升代码的可读性和可维护性,使得开发者可以更加清晰地了解每个组件的样式依赖关系,而且还能够确保只加载真正需要的样式表。

此外,样式库和与打包器集成的样式工具也可以采用这一新特性。即使开发者不直接渲染自己的样式表,只要他们所使用的工具升级到支持这一功能,他们同样能够享受到这一改进带来的好处。

3)异步脚本支持

在HTML中,普通脚本(<script src="...">)和延迟脚本(<script defer src="...">)按照文档顺序加载,这限制了它们在组件树深处的灵活使用。然而,异步脚本能够以任意顺序加载,为开发者提供了更大的灵活性。

React 19 针对异步脚本提供了增强的支持,允许开发者在组件树的任何位置渲染它们,直接放在实际依赖该脚本的组件内部。这大大简化了脚本的管理,无需再担心脚本实例的重新定位和去重问题。

现在,你可以在组件中这样使用异步脚本:

javascript 复制代码
function MyComponent() {  
  return (  
    <div>  
      <script async src="..." />  
      Hello World  
    </div>  
  );  
}  
  
function App() {  
  return (  
    <html>  
      <body>  
        <MyComponent />  
        ...  
        <MyComponent /> // 不会导致DOM中出现重复的脚本  
      </body>  
    </html>  
  );  
}

在所有渲染环境中,异步脚本都将进行去重处理,因此即使多个不同的组件渲染了同一个脚本,React 也只会加载和执行该脚本一次。

在服务端渲染中,异步脚本将被包含在<head>标签中,并优先于阻止绘制的更关键资源(如样式表、字体和图像预加载)之后加载。

6. 资源加载

通常情况下,浏览器首先渲染视图,然后是样式表、字体和图像。这可能导致从未样式化(或未样式化内容的闪烁)到样式化视图的闪烁。

为了解决这个问题,开发人员通常会添加自定义代码来检测这些资源何时准备就绪,确保仅在一切加载完毕后显示视图。

在 React 19 中,图像和其他文件将在用户浏览当前页面时在后台加载。这个改进应该有助于改善页面加载时间并减少等待时间。

此外,React 还引入了用于资源加载的生命周期 Suspense,包括脚本、样式表和字体。这个特性使 React 能够确定内容何时准备好显示,消除了任何"未样式化"闪烁。

还有新的资源加载 API,如 preload 和 preinit,可以提供更大的控制权,使资源何时加载和初始化。

通过允许资源在后台异步加载,React 19 将最小化等待时间,并确保用户可以在没有中断的情况下与内容交互。这种优化不仅提升了 React 应用程序的性能,也为用户提供了更愉快的浏览体验。

7. 新的 React Hooks

React19 想要彻底改变我们在项目开发中的 UI 交互方式。因此对于 React19 而言,Suspense、ErrorBoundary、Action 的重要性将会变得越来越高。

React 19 之前,React 高手与普通开发者之间,开发的项目无论是从性能上、还是从代码优雅上差距都非常大。因此不同的人对于 React 的感受完全不一样。

而 React 19 则借由推出一些新的 hook,暗中传递一种框架思维「最佳实践」,这将会极大的拉进普通开发者与顶尖高手之间的差距。对于大多数 React 开发者而言,这会是一个极大的提升。

这一意图在 React 新的官方文档与 Next.js 中提现得非常明显

React 19在钩子函数方面也进行了增强,新增了use钩子。因此使用 useMemo、forwardRef、useEffect 和 useContext 的方式将发生变化。

这个钩子让我们在处理异步操作和资源获取更为便捷。通过use钩子,开发者可以在组件中直接挂载资源,如通过promise获取数据,React将暂停该组件的渲染直到数据加载完成,从而简化了代码结构并减少了不必要的渲染。

React19 的 大部分更新,几乎都是围绕如何在开发中尽量不用或者少用 useEffect 来展开。在之前的项目开发中,useEffect 是我们处理异步问题必须使用的重要 hook 之一,他几乎存在于每一个页面组件之中。

React 19 则引入了新的 hook,让我们处理异步开发时,不再需要 useEffect,从而极大的改变我们的开发方式。

使用 use 钩子处理异步数据

在React 19中,use 钩子可以让我们更简洁地处理异步数据。下面是一个使用use钩子来请求并展示用户数据的示例:

javascript 复制代码
import { use } from 'react';  // 引入use钩子

function UserProfile({ userId }) {
  const userPromise = fetch(`https://api.example.com/users/${userId}`)
    .then(response => response.json());

  // 使用use钩子处理异步请求
  const user = use(userPromise);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Location: {user.location}</p>
    </div>
  );
}

export default UserProfile;

use钩子允许组件在渲染过程中直接使用由 promise 返回的数据。当数据加载完成时,组件将重新渲染以显示数据。

对比旧的异步处理方式,在React 19之前,处理异步数据通常需要使用useState和useEffect钩子,如下例子所示:

javascript 复制代码
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }, [userId]);  // 依赖项数组中的userId确保当userId变化时重新发起请求

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Location: {user.location}</p>
    </div>
  );
}

export default UserProfile;

在这种传统方法中,我们需要管理一个状态变量user来存储用户数据,并使用useEffect来处理异步请求和响应。这不仅增加了代码量,也增加了组件的复杂度。

例子2:

在项目开发中,新页面渲染时请求一个接口的场景非常常见。新的架构思维的开发代码如下所示。

首先我们需要定义一个 API 用于请求数据。

javascript 复制代码
const api = async () => {
  const res = await fetch('https://api.chucknorris.io/jokes/random')
  return res.json()
}

然后创建一个函数组件,并执行该 api

javascript 复制代码
export default function Index() {
  const __api = api()
  return (
    <div>
      <div id='tips'>初始化时,自动获取一条数据内容</div>
      <Suspense fallback={<div>loading...</div>}>
        <Item api={__api} />
      </Suspense>
    </div>
  )
}

最后在子组件中,获取 api 执行之后得到的数据

javascript 复制代码
const Item = (props) => {
  const joke = use(props.api)
  return (
    <div>
      <div>{joke.value}</div>
    </div>
  )
}

大家可以自行感受一下新的开发方式与以前基于 useEffect 请求数据有什么不同之处。

**例子3:**不再使用 useContext(),而是使用 use(context)。

javascript 复制代码
import { createContext, useState, use } from 'react';

const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const Card = () => {
  // use Hook()
  const { theme, toggleTheme } = use(ThemeContext);

  return (
    <div
      className={`p-4 rounded-md ${
        theme === 'light' ? 'bg-white' : 'bg-gray-800'
      }`}
    >
      <h1
        className={`my-4 text-xl ${
          theme === 'light' ? 'text-gray-800' : 'text-white'
        }`}
      >
        Theme Card
      </h1>
      <p className={theme === 'light' ? 'text-gray-800' : 'text-white'}>
       Hello!! use() hook
      </p>
      <button
        onClick={toggleTheme}
        className='bg-blue-500 hover:bg-blue-600 text-white rounded-md mt-4 p-4'
      >
        {theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
      </button>
    </div>
  );
};

const Theme = () => {
  return (
    <ThemeProvider>
      <Card />
    </ThemeProvider>
  );
};

export default Theme

8. 功能改进

1)ref 作为属性

ref 现在将作为 props 传递,而不是使用 forwardRef() 钩子。这将简化代码。因此,在 React19 之后,你将不再需要使用 forwardRef()。

之前:这是在 React 19 之前使用 forwardRef() 的示例:
javascript 复制代码
import React, { forwardRef } from 'react';

const ExampleButton = forwardRef((props, ref) => (
  <button ref={ref}>
    {props.children}
  </button>
));
之后:ref 可以作为 props 传递。不再需要 forwardRef()。
javascript 复制代码
import React from 'react';

const ExampleButton = ({ ref, children }) => (
  <button ref={ref}>
    {children}
  </button>
);

//...  
  
<ExampleButton ref={ref} />

2)作为提供者

React 19 允许直接将 <Context> 用作提供者,而无需使用传统的 <Context.Provider> 写法:

javascript 复制代码
const ThemeContext = createContext('');  
  
function App({children}) {  
  return (  
    <ThemeContext value="dark">  
      {children}  
    </ThemeContext>  
  );  
}

这种新的语法更加简洁直观。为了方便开发者升级现有代码,React 团队将发布一个代码转换工具,能够自动将现有的 <Context.Provider> 转换为新的 <Context> 提供者。未来版本中,将逐步弃用 <Context.Provider>,以推动 React 社区向更加简化的语法过渡。

3)refs 清理函数

现在支持从 ref 回调函数中返回一个清理函数:

javascript 复制代码
<input  
  ref={(ref) => {  
    // 创建 ref  
  
    // 新增:返回一个清理函数,当元素从 DOM 中移除时重置 ref。  
    return () => {  
      // ref 的清理工作  
    };  
  }}  
/>

当组件卸载时,React 将调用从 ref 回调函数中返回的清理函数。这适用于 DOM refs、类组件的 refs 以及 useImperativeHandle。

由于引入了 ref 清理函数的机制,现在 TypeScript 将拒绝从 ref 回调函数中返回除清理函数以外的任何内容。为了避免这个问题,我们通常建议避免使用隐式返回,比如将赋值操作放在花括号中,如下所示:

  • 原来的写法:
javascript 复制代码
<div ref={current => (instance = current)} />
  • 优化后的写法:
javascript 复制代码
<div ref={current => { instance = current; }} />

这种改变是因为 TypeScript 无法判断原始代码中返回的是否应该是清理函数,还是无意中的隐式返回值。通过将赋值操作明确地包裹在花括号中,确保了 ref 回调中不会意外地返回任何值,除非有意为之。

为了自动化这种模式的转换,可以使用 no-implicit-ref-callback-return 规则进行代码转换。这将帮助你在升级 React 版本时更顺畅地处理 ref 相关的代码。

4)useDeferredValue 的初始值

React 19 为 useDeferredValue 引入了 initialValue 选项,该选项允许指定组件首次渲染时返回的值。

javascript 复制代码
function Search({ deferredValue }) {  
  // 在组件首次渲染时,返回 initialValue 作为 value。  
  // 随后,useDeferredValue 会在后台计划一次重渲染,使用 deferredValue 作为新的 value。  
  const value = useDeferredValue(deferredValue, { initialValue: '' });  
  
  return (  
    <Results query={value} />  
  );  
}

使用 initialValue 可以确保组件在首次渲染时能够立即显示一个占位值,而无需等待 deferredValue 的异步计算完成。随后,当 deferredValue 准备好时,useDeferredValue 会触发组件的后台重渲染,以显示最新的值。这有助于提升应用的响应性和用户体验。

5)资源预加载支持

新 API :preXXX 预加载资源

重要性:★★

参考信息:preconnect/ prefetchDNS / preinit / preinitModule / preload / preloadModule 文档说明

在文档初始加载和客户端更新时,及时告知浏览器可能即将需要加载的资源,对于提升页面性能至关重要。React 19 引入了一系列新的API,旨在简化浏览器资源的加载和预加载过程,让开发者能够构建出流畅且高效的用户体验。

javascript 复制代码
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';  
  
function MyComponent() {  
  preinit('https://.../path/to/some/script.js', { as: 'script' }); // 提前加载并执行脚本  
  preload('https://.../path/to/font.woff', { as: 'font' }); // 预加载字体  
  preload('https://.../path/to/stylesheet.css', { as: 'style' }); // 预加载样式表  
  prefetchDNS('https://...'); // 当可能会从该主机请求资源但尚不确定时  
  preconnect('https://...'); // 当确定会从该主机请求资源但不确定具体资源时  
}

这些 API 调用会在渲染组件时生成相应的DOM标签,如下所示:

html 复制代码
<html>  
  <head>  
    <!-- 链接根据其对页面加载的贡献程度进行优先级排序 -->  
    <link rel="prefetch-dns" href="https://..."> <!-- DNS预获取 -->  
    <link rel="preconnect" href="https://..."> <!-- 提前建立连接 -->  
    <link rel="preload" as="font" href="https://.../path/to/font.woff"> <!-- 预加载字体 -->  
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css"> <!-- 预加载样式表 -->  
    <script async src="https://.../path/to/some/script.js"></script> <!-- 异步加载并执行脚本 -->  
  </head>  
  <body>  
    ...  
  </body>  
</html>

通过利用这些API,开发者可以优化页面的初始加载速度,减少用户等待时间。同时,在客户端更新时,预取和预加载资源也能帮助加快导航速度,提升用户体验。无论是通过预加载字体和样式表来减少页面渲染阻塞,还是通过预取DNS和预连接来加速资源请求,这些API都为开发者提供了强大的工具,使资源加载更加智能和高效。

分类来看:

  • preconnect 、prefetchDNS 等 API 将插入 <link rel="preconnect" /> 标签的行为命令化,但由于预加载时机越早越好,等到 React 应用运行后,已经错过了 preconnect 的最好时机,所以编写至 HTML 内或 SSR 仍然是首选方案,此类 API 不重要。
  • preinit / preload 等 API 将 脚本、样式 的加载命令化,但现代项目开发时已经内部模块化,使用 await import() 等懒加载行为已足够,故除了需要加载外部 脚本、样式 外,此 API 也不重要。

6)与第三方脚本和扩展的兼容性

在React 19中,对挂载过程进行了优化,以更好地兼容第三方脚本和浏览器扩展。

在客户端挂载时,如果渲染的元素与服务器返回的HTML中的元素不一致,React会触发客户端的重新渲染,以确保内容的正确性。然而,过去,若第三方脚本或浏览器扩展插入了某些元素,这会导致不匹配错误并触发不必要的客户端渲染。

现在,React 19 能够智能地跳过<head>和<body>中的意外标签,从而避免了因这些元素引发的不匹配错误。即使因为其他原因需要进行整个文档的重新渲染,React也会保留由第三方脚本和浏览器扩展插入的样式表,确保页面的完整性和一致性。

7)水合错误报告

React 19 在 react-dom 中对水合错误的报告进行了优化。过去,在开发模式下遇到水合不匹配时,系统往往只记录多个错误,而缺乏关于不匹配内容的具体信息。现在,引入了 diff 功能,使得客户端渲染与服务端渲染内容之间的差异一目了然。这一改进不仅提升了错误报告的清晰度,更有助于开发者迅速定位并修复水合相关问题,从而大幅提升开发效率。

现在只会记录一条包含不匹配内容差异的消息:

8)更好的错误报告

为了提供更细粒度的错误处理控制,还新增了两个根选项来与onRecoverableError相辅相成:

  • onCaughtError:当React在错误边界中成功捕获到错误时,此选项将被调用。
  • onUncaughtError:当错误被抛出且未能被任何错误边界捕获时,此选项将被触发。
  • onRecoverableError:当错误发生但React能够自动恢复时,该选项将起作用。

参考信息:onCaughtError / onUncaughtError / onRecoverableError 文档说明

这些新增选项不仅增强了React的错误处理能力,还赋予了开发者在不同错误场景下执行特定逻辑的能力。无论是进行错误日志的记录、发送错误报告,还是执行其他自定义操作,这些选项都能满足开发者的需求,帮助他们更有效地管理和应对React应用中的错误情况。

给 React 在全局 ReactDOM.createRoot 挂载时开放了一个全局捕获错误的出口:

javascript 复制代码
ReactDOM.createRoot(document.getElementById('root')!, {
  onCaughtError: (error, errorInfo) => {
    // ...
  },
})

这可以避免在 ErrorBoundary 内部收集错误,而在全局更清真的统一处理(此处只是单纯报告错误,涉及到复杂的 ErrorBoundary 错误恢复和重试,仍然需要编写大量代码)。

注:不可以捕获 React 内的异步逻辑错误。

React 19 对错误处理进行了优化,旨在消除错误信息的重复,并为处理捕获和未捕获的错误提供了更多选项。例如,当渲染过程中发生错误并被错误边界捕获时,以前 React 会重复抛出相同的错误(一次是原始错误,另一次是在尝试自动恢复失败后),然后调用console.error输出错误发生位置的信息。

这种处理方式导致每个被捕获的错误都会被报告三次:

在 React 19 中将记录单个错误,并包含所有错误信息:

9)useRef 必须传入默认值

重要性:★★★

参考信息:useRef 入参更改说明

React v19 要求 useRef(defaultValue) 传入一个默认值,这会影响到所有历史 useRef() 未设定默认值的调用写法。

在以前,我们往往会通过 useRef<HTMLInputElement>(null!) 技巧 避免类型提示为空,因为此处 ref 必定存在,若存在默认值或采用技巧,则不存在此变动带来的问题,但相当一部分人并不会采用 React TypeScript 技巧 ,请多加留意。

10)React UMD 版本已被删除

重要性:★★★

参考信息:删除 UMD

仍有不少项目希望通过 external React UMD 的方式进行一些依赖外置化加载的设计,但未来 esm 是大势所趋,故 React v19 已删除 UMD 版本。

如还想通过外置形式使用 React ,需改为 esm 的 <script type="module"> 方式使用:

html 复制代码
<script type="module">
  import React from "https://esm.sh/react@beta"
  import ReactDOM from "https://esm.sh/react-dom@beta/client"
  ...
</script>

各种不重要的小变化

API 相关
  • ReactDOM.render / ReactDOM.hydrate 旧版渲染应用 API 已被删除( 链接 )

  • useDeferredValue 支持设定默认值( 链接 )

  • React.createFactory 已被删除( 链接 )

  • react-test-renderer/shallow 导出位置已被更改( 链接 )

  • unmountComponentAtNode 旧版卸载节点 API 已被删除( 链接 )

  • ReactDOM.findDOMNode API 已被删除( 链接 )

特性相关
  • 更好的 web components 支持( 链接 )

注:这是一个较瞩目的新特性,但大多数人很少使用 web component ,故只有特定用户群体才值得去关注。

体验相关
  • 水合出现差异时,报错信息更友好( 链接 )。
  • 优化了控制台错误打印的格式( 链接 )
  • useLayoutEffect 在服务端运行的警告已被删除( 链接 )
Class 组件相关
  • Class 组件的 propTypes / defaultProps 已被删除( 链接 )
  • Class 组件的 getChildContext 已被删除( 链接 )
  • Class 组件的字符串 ref="string" 已被删除( 链接 )
函数组件相关
  • 函数组件的 render() 渲染方式已被删除( 链接 )
测试相关
  • act 函数导出位置已转移至 react ( 链接 )
  • react-test-renderer 已弃用( 非删除, 链接 )

注:此包以前可能用于一些简单的组件测试,但现在已被弃用,请在所有 React 的测试中统一使用 RTL ( @testing-library/react ),这将是官方推荐的唯一测试库。

类型相关
  • ReactElement 类型被调整( 链接 )
  • 限定 React JSX 类型的命名空间( 链接 )
  • 注:为了避免各种 JSX 的类型在全局冲突,Vue v3.4 也限制了自己的命名空间。
  • useReducer 类型改进( 链接 )

新增api参考