y.y
Published on

React v19

React v19

2024年12月5日 由 React 团队 发布


注意

React 19 现已稳定!

自4月份 React 19 RC 版本发布以来的新增内容:

本文的日期已更新以反映稳定版发布日期。

React v19 现已在 npm 上发布!

在我们的 React 19 升级指南 中,我们分享了将应用升级到 React 19 的分步说明。在本文中,我们将概述 React 19 中的新特性,以及如何采用这些特性。

关于破坏性变更的列表,请参见 升级指南

React 19 的新特性

Actions (操作)

React 应用中的一个常见用例是执行数据变更然后更新状态。例如,当用户提交表单更改他们的姓名时,你需要发起 API 请求,然后处理响应。在过去,你需要手动处理 pending 状态、错误、乐观更新和连续请求。

例如,你可以使用 useState 来处理 pending 和错误状态:

// Actions 之前
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    }
    
    redirect("/path");
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

在 React 19 中,我们添加了对在 transitions 中使用异步函数的支持,以自动处理 pending 状态、错误、表单和乐观更新。

例如,你可以使用 useTransition 来帮助你处理 pending 状态:

// 使用 Actions 的 pending 状态
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      }
      
      redirect("/path");
    })
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

异步 transition 会立即将 isPending 状态设置为 true,发起异步请求,并在所有 transitions 完成后将 isPending 切换为 false。这让你可以在数据变更时保持当前 UI 的响应性和交互性。

注意

按照惯例,使用异步 transitions 的函数被称为 "Actions"。

Actions 自动为你管理数据提交:

  • Pending 状态: Actions 提供一个 pending 状态,它在请求开始时启动,并在最终状态更新提交时自动重置。
  • 乐观更新: Actions 支持新的 useOptimistic hook,让你可以在请求提交期间向用户显示即时反馈。
  • 错误处理: Actions 提供错误处理功能,当请求失败时你可以显示 Error Boundaries,并自动将乐观更新恢复到原始值。
  • 表单: <form> 元素现在支持将函数传递给 actionformAction 属性。传递给 action 属性的函数默认使用 Actions 并在提交后自动重置表单。

在 Actions 的基础上,React 19 引入了 useOptimistic 来管理乐观更新,以及一个新的 hook React.useActionState 来处理 Actions 的常见情况。在 react-dom 中,我们添加了 <form> Actions 来自动管理表单和 useFormStatus 来支持表单中 Actions 的常见情况。

在 React 19 中,上面的例子可以简化为:

// 使用 <form> Actions 和 useActionState
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />  
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

在下一部分中,我们将详细介绍 React 19 中每个新的 Action 特性。

新的 hook: useActionState

为了让 Actions 的常见用例更简单,我们添加了一个新的名为 useActionState 的 hook:

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    const error = await updateName(newName);
    if (error) {
      // 你可以返回 action 的任何结果。
      // 这里,我们只返回错误。
      return error;
    }
    // 处理成功
    return null;
  },
  null,
);

useActionState 接收一个函数("Action"),并返回一个包装后的 Action 供调用。之所以可以这样做是因为 Actions 可以组合。当包装的 Action 被调用时,useActionState 将返回 Action 的最后一个结果作为 data,以及 Action 的 pending 状态作为 pending

注意

React.useActionState 在 Canary 版本中原本叫做 ReactDOM.useFormState,但我们已将其重命名并废弃了 useFormState

更多信息见 #28491

关于更多信息,请参见 useActionState 的文档。

React DOM: <form> Actions

Actions 也集成到了 React 19 的新 <form> 特性中的 react-dom 中。我们添加了对将函数作为 <form><input><button> 元素的 actionformAction 属性的支持,以自动使用 Actions 提交表单:

<form action={actionFunction}>

<form> Action 成功时,React 将自动重置非受控组件的表单。如果你需要手动重置 <form>,你可以调用新的 React DOM API requestFormReset

React DOM:新的 hook: useFormStatus

在设计系统中,常见的情况是编写需要访问表单信息的设计组件,而无需向下传递 props。这可以通过 Context 来完成,但为了让常见情况更简单,我们添加了一个新的 hook useFormStatus:

import {useFormStatus} from 'react-dom';

function DesignButton() {
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

useFormStatus 会读取父级 <form> 的状态,就像表单是一个 Context provider 一样。

更多信息请参见 react-dom 文档中的 useFormStatus

新的 hook: useOptimistic

在执行数据变更时,另一个常见的 UI 模式是在异步请求进行时乐观地显示最终状态。在 React 19 中,我们添加了一个名为 useOptimistic 的新 hook 来简化这一过程:

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);
  
  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

useOptimistic hook 会在 updateName 请求进行时立即渲染 optimisticName。当更新完成或出错时,React 会自动切换回 currentName 值。

更多信息请参见 useOptimistic 的文档。

新的 API: use

在 React 19 中,我们引入了一个新的 API 来在渲染时读取资源: use

例如,你可以用 use 读取一个 promise,React 将会暂停直到 promise 解决:

import {use} from 'react';

function Comments({commentsPromise}) {
  // `use` 将暂停直到 promise 解决
  const comments = use(commentsPromise);
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // 当 `use` 在 Comments 中暂停时,
  // 这个 Suspense 边界将被显示
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

注意

use 不支持在渲染时创建的 promises

如果你试图将在渲染时创建的 promise 传递给 use,React 将会警告:

Console

A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.

要修复这个问题,你需要传入一个来自支持缓存 promises 的 suspense 支持库或框架的 promise。在未来,我们计划推出使在渲染时缓存 promises 更容易的功能。

你也可以用 use 读取 context,允许你在诸如提前返回之后等条件下读取 Context:

import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  // 这在使用 useContext 时无法工作
  // 因为有提前返回
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}

use API 只能在渲染时调用,类似于 hooks。与 hooks 不同的是,use 可以有条件地调用。在未来,我们计划支持更多使用 use 在渲染时消费资源的方式。

更多信息请参见 use 的文档。

新的 React DOM 静态 API

我们在 react-dom/static 中添加了两个用于静态站点生成的新 API:

这些新 API 改进了 renderToString,它们会等待数据加载完成再生成静态 HTML。它们被设计为可以在 Node.js Streams 和 Web Streams 等流式环境中工作。例如,在 Web Stream 环境中,你可以使用 prerender 将 React 树预渲染为静态 HTML:

import { prerender } from 'react-dom/static';

async function handler(request) {
  const {prelude} = await prerender(<App />, {
    bootstrapScripts: ['/main.js']
  });

  return new Response(prelude, {
    headers: { 'content-type': 'text/html' },
  });
}

Prerender API 会等待所有数据加载完成后再返回静态 HTML 流。这些流可以被转换为字符串,或者通过流式响应发送。它们不支持在加载时流式传输内容,这一点在现有的 React DOM 服务端渲染 API 中是支持的。

更多信息请参见 React DOM 静态 API

React 服务器组件

服务器组件

服务器组件是一个新选项,它允许在打包之前,在与客户端应用或 SSR 服务器分开的环境中提前渲染组件。React 服务器组件中的"服务器"就是指这个独立的环境。服务器组件可以在 CI 服务器上的构建时运行一次,或者使用 web 服务器为每个请求运行。

React 19 包含了从 Canary 通道中包含的所有 React 服务器组件功能。这意味着附带服务器组件的库现在可以将 React 19 作为具有 react-server 导出条件的对等依赖,以在支持全栈 React 架构的框架中使用。

注意

如何构建对服务器组件的支持?

虽然 React 19 中的 React 服务器组件是稳定的,并且不会在主要版本之间发生破坏性变化,但用于实现 React 服务器组件打包器或框架的底层 API 不遵循语义化版本控制,可能会在 React 19.x 的次要版本之间发生变化。

要作为打包器或框架支持 React 服务器组件,我们建议固定到特定的 React 版本,或使用 Canary 发布版本。我们将继续与打包器和框架合作,以在未来稳定用于实现 React 服务器组件的 API。

更多信息请参见 React 服务器组件 的文档。

服务器 Actions

服务器 Actions 允许客户端组件调用在服务器上执行的异步函数。

当使用 "use server" 指令定义服务器 Action 时,你的框架会自动创建对服务器函数的引用,并将该引用传递给客户端组件。当在客户端调用该函数时,React 将发送请求到服务器执行该函数,并返回结果。

注意

服务器组件没有指令。

一个常见的误解是认为服务器组件是由 "use server" 标记的,但服务器组件实际上没有指令。"use server" 指令是用于服务器 Actions 的。

更多信息请参见 指令 的文档。

服务器 Actions 可以在服务器组件中创建并作为 props 传递给客户端组件,也可以在客户端组件中导入和使用。

更多信息请参见 React 服务器 Actions 的文档。

React 19 中的改进

ref 作为 prop

从 React 19 开始,你现在可以在函数组件中将 ref 作为 prop 访问:

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

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

新的函数组件将不再需要 forwardRef,我们将发布一个 codemod 来自动更新你的组件以使用新的 ref prop。在未来的版本中,我们将废弃并移除 forwardRef

注意

传递给类组件的 refs 不会作为 props 传递,因为它们引用的是组件实例。

hydration 错误的差异对比

我们还改进了 react-dom 中 hydration 错误的报告。例如,不再是在 DEV 环境下记录多个没有任何不匹配信息的错误:

Console

Warning: Text content did not match. Server: "Server" Client: "Client" at span at App

Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.

Warning: Text content did not match. Server: "Server" Client: "Client" at span at App

Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.

Uncaught Error: Text content does not match server-rendered HTML. at checkForUnmatchedText …

我们现在记录一条包含不匹配差异的单一消息:

Console

Uncaught Error: Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if an SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch

<App>
  <span>
    + Client
    - Server
at throwOnHydrationMismatch …

<Context> 作为 provider

在 React 19 中,你可以渲染 <Context> 作为 provider 而不是 <Context.Provider>:

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );
}

新的 Context providers 可以使用 <Context>,我们将发布一个 codemod 来转换现有的 providers。在未来的版本中,我们将废弃 <Context.Provider>

refs 的清理函数

我们现在支持从 ref 回调返回清理函数:

<input
  ref={(ref) => {
    // ref 创建
    // 新特性: 返回一个清理函数来重置
    // ref 当元素从 DOM 中移除时。
    return () => {
      // ref 清理
    };
  }}
/>

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

注意

之前,React 会在卸载组件时用 null 调用 ref 函数。如果你的 ref 返回一个清理函数,React 现在将跳过这一步。

在未来的版本中,我们将废弃在卸载组件时用 null 调用 refs。

由于引入了 ref 清理函数,从 ref 回调返回任何其他内容现在都会被 TypeScript 拒绝。修复方法通常是停止使用隐式返回,例如:

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

原始代码返回了 HTMLDivElement 的实例,而 TypeScript 无法知道这是否应该是一个清理函数或者你是否不想返回清理函数。

你可以使用 no-implicit-ref-callback-return 来对这种模式进行代码修改。

useDeferredValue 初始值

我们为 useDeferredValue 添加了一个 initialValue 选项:

function Search({deferredValue}) {
  // 在初始渲染时值为 ''。
  // 然后使用 deferredValue 安排一次重新渲染。
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

当提供了 initialValue 时,useDeferredValue 会在组件的初始渲染时将其作为 value 返回,并在后台安排一次使用 deferredValue 的重新渲染。

更多信息请参见 useDeferredValue

对文档元数据的支持

在 HTML 中,文档元数据标签如 <title><link><meta> 被保留用于放置在文档的 <head> 部分。在 React 中,决定哪些元数据适合应用的组件可能离渲染 <head> 的地方很远,或者 React 根本不渲染 <head>。在过去,这些元素需要在 effect 中手动插入,或通过像 react-helmet 这样的库来插入,并且在服务端渲染 React 应用时需要谨慎处理。

在 React 19 中,我们添加了对在组件中原生渲染文档元数据标签的支持:

function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

当 React 渲染这个组件时,它会发现 <title><link><meta> 标签,并自动将它们提升到文档的 <head> 部分。通过原生支持这些元数据标签,我们能够确保它们可以与仅客户端的应用、流式 SSR 和服务器组件一起工作。

注意

你可能仍然需要一个元数据库

对于简单的用例,将文档元数据渲染为标签可能已经足够,但库可以提供更强大的功能,比如基于当前路由用特定的元数据覆盖通用元数据。这些功能使得框架和库(如 react-helmet)更容易支持元数据标签,而不是替代它们。

更多信息请参见 <title><link><meta> 的文档。

对样式表的支持

样式表,无论是外部链接的(<link rel="stylesheet" href="...">)还是内联的(<style>...</style>),都需要在 DOM 中仔细定位,因为样式优先级规则。构建一个允许在组件中进行组合的样式表功能很困难,所以用户经常最终要么在远离可能依赖它们的组件的地方加载所有样式,要么使用封装了这种复杂性的样式库。

在 React 19 中,我们正在解决这种复杂性,并通过内置对样式表的支持,提供与客户端并发渲染和服务器流式渲染更深层次的集成。如果你告诉 React 你的样式表的 precedence,它将管理样式表在 DOM 中的插入顺序,并确保在显示依赖这些样式规则的内容之前加载外部样式表。

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" />  {/* 将被插入在 foo 和 bar 之间 */}
    </div>
  );
}

在服务端渲染期间,React 会将样式表包含在 <head> 中,这确保浏览器在完成加载之前不会进行渲染。如果样式表是在我们已经开始流式传输后才被发现的,React 会确保在显示依赖该样式表的 Suspense 边界的内容之前,将样式表插入到客户端的 <head> 中。

在客户端渲染期间,React 会等待新渲染的样式表加载完成后再提交渲染。如果你从应用程序的多个位置渲染这个组件,React 将只在文档中包含样式表一次:

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

对于习惯于手动加载样式表的用户来说,这是一个机会,可以将这些样式表放在依赖它们的组件旁边,这样可以更好地进行本地推理,并更容易确保你只加载实际依赖的样式表。

样式库和与打包工具的样式集成也可以采用这个新功能,所以即使你不直接渲染自己的样式表,当你的工具升级以使用这个功能时,你仍然可以受益。

有关更多详细信息,请阅读 <link><style> 的文档。

对异步脚本的支持

在 HTML 中,普通脚本(<script src="...">)和延迟脚本(<script defer="" src="...">)按文档顺序加载,这使得在组件树深处渲染这些类型的脚本变得具有挑战性。然而,异步脚本(<script async="" src="...">)将以任意顺序加载。

在 React 19 中,我们增加了对异步脚本的更好支持,允许你在组件树的任何地方渲染它们,就在实际依赖脚本的组件内部,而无需管理脚本实例的重定位和去重:

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  );
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> // 不会导致 DOM 中出现重复脚本
    </body>
  </html>
}

在所有渲染环境中,异步脚本都会被去重,这样即使脚本被多个不同的组件渲染,React 也只会加载和执行脚本一次。

在服务端渲染中,异步脚本将被包含在 <head> 中,并在更关键的资源(如样式表、字体和图片预加载)之后进行优先级排序。

更多详细信息,请阅读 <script> 的文档。

资源预加载的支持

在初始文档加载和客户端更新期间,尽早告诉浏览器它可能需要加载的资源,可以对页面性能产生巨大影响。

React 19 包含了许多用于加载和预加载浏览器资源的新 API,使构建出色的体验变得尽可能简单,而不会因资源加载效率低下而受到阻碍。

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://...') // 当你将请求某些内容但不确定是什么时
}
<!-- 上述代码会在 DOM/HTML 中产生以下结果 -->
<html>
  <head>
    <!-- links/scripts 按其对早期加载的实用性排序,而不是按调用顺序 -->
    <link rel="prefetch-dns" href="https://...">
    <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 可以用来优化初始页面加载,方法是将字体发现从样式表加载中移出。它们还可以通过预取预期导航所需的资源列表,然后在点击或甚至悬停时急切预加载这些资源,使客户端更新更快。

更多详细信息请参见 资源预加载 API

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

我们改进了 hydration 以适应第三方脚本和浏览器扩展。

在 hydration 时,如果在客户端渲染的元素与服务器返回的 HTML 中的元素不匹配,React 将强制进行客户端重新渲染以修复内容。以前,如果第三方脚本或浏览器扩展插入了元素,它会触发不匹配错误和客户端渲染。

在 React 19 中,会跳过 <head><body> 中意外出现的标签,避免不匹配错误。如果 React 需要由于不相关的 hydration 不匹配而重新渲染整个文档,它会保留第三方脚本和浏览器扩展插入的样式表。

更好的错误报告

我们在 React 19 中改进了错误处理,以消除重复并提供处理已捕获和未捕获错误的选项。例如,当渲染中的错误被 Error Boundary 捕获时,之前 React 会抛出错误两次(一次是原始错误,然后在自动恢复失败后再次抛出),然后调用 console.error 显示错误发生的位置。

这导致每个被捕获的错误产生三个错误:

Console

Uncaught Error: hit at Throws at renderWithHooks …

Uncaught Error: hit <-- 重复 at Throws at renderWithHooks …

以上错误发生在 Throws 组件中:
at Throws at ErrorBoundary at App
React 将尝试使用你提供的错误边界 ErrorBoundary 从头开始重新创建此组件树。

在 React 19 中,我们只记录一个包含所有错误信息的错误:

Console

Error: hit at Throws at renderWithHooks …
以上错误发生在 Throws 组件中:
at Throws at ErrorBoundary at App
React 将尝试使用你提供的错误边界 ErrorBoundary 从头开始重新创建此组件树。
at ErrorBoundary at App

此外,我们添加了两个新的根选项来补充 onRecoverableError

  • onCaughtError:当 React 在 Error Boundary 中捕获错误时调用。
  • onUncaughtError:当错误被抛出且未被 Error Boundary 捕获时调用。
  • onRecoverableError:当错误被抛出并自动恢复时调用。

更多信息和示例,请参见 createRoothydrateRoot 的文档。

对自定义元素的支持

React 19 添加了对自定义元素的完整支持,并在 Custom Elements Everywhere 上通过了所有测试。

在过去的版本中,在 React 中使用自定义元素一直很困难,因为 React 将未识别的 props 作为属性而不是属性处理。在 React 19 中,我们添加了属性支持,它在客户端和 SSR 期间都可以工作,采用以下策略:

  • 服务端渲染:如果传递给自定义元素的 props 类型是原始值如 stringnumber 或值为 true,则会渲染为属性。具有非原始类型如 objectsymbolfunction 或值为 false 的 props 将被省略。

  • 客户端渲染:与自定义元素实例上的属性匹配的 props 将被赋值为属性,否则它们将被赋值为属性。

感谢 Joey Arhar 推动了 React 中自定义元素支持的设计和实现。

如何升级

请参阅 React 19 升级指南 获取分步说明和完整的破坏性变更与重要变更列表。

注意:本文最初发布于 2024 年 4 月 25 日,已更新至 2024 年 12 月 5 日的稳定版发布。