React 19 正式发布

自从 4 月份 React 发布了 19 RC 版本后,经过 8 个月的漫长等待,React 终于发布了 19 的正式版本,我们一起来看看 React 19 正式版本的更新内容。


概览

React 19 引入了许多新功能和改进,包括支持异步函数的 Actions、用于乐观更新的 useOptimistic 钩子、新的 useActionState 钩子、改进的表单处理、以及新的 use API。新增的 React DOM 静态 API 和服务器组件功能使得静态站点生成和服务器端渲染更加高效。此外,React 19 还改进了 ref 的处理、支持文档元数据和样式表的渲染、以及对异步脚本和资源预加载的支持。错误报告和第三方脚本兼容性也得到了增强。

  1. Actions:自动处理挂起状态、错误、表单和乐观更新。

  2. 新钩子useActionStateuseOptimistic 和 useFormStatus 简化了状态管理和表单处理。

  3. use API:用于在渲染时读取资源,支持异步操作。

  4. React DOM 静态 APIprerender 和 prerenderToNodeStream 优化静态站点生成。

  5. 服务器组件:支持在服务器上执行异步函数,增强全栈 React 架构。

  6. ref 作为属性:简化了函数组件中的 ref 处理。

  7. 文档元数据和样式表支持:允许在组件中原生渲染 <title><link> 和 <meta> 标签,以及样式表。

  8. 异步脚本支持:改进了异步脚本的加载和执行。

  9. 预加载资源:通过 prefetchDNSpreconnectpreload 和 preinit API 提升页面性能。

  10. 错误报告和兼容性:改进了错误处理和与第三方脚本、浏览器扩展的兼容性。

  11. 自定义元素支持:全面支持自定义元素,简化了属性和属性的处理。


正文如下:

React 19 的新增功能

Actions

在 React 应用中,常见的用例是执行数据变更并更新响应状态。例如,当用户提交表单更改名字时,你需要发出 API 请求并处理响应。在过去,你需要手动处理挂起状态、错误、乐观更新和顺序请求。

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

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}>
        更新
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

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

例如,你可以使用 useTransition 来处理挂起状态:

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}>
        更新
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

异步过渡会立即将 isPending 状态设置为 true,进行异步请求,并在任何过渡完成后将 isPending 切换为 false。这使你可以在数据更改的同时保持当前 UI 响应和交互。

按照惯例,使用异步过渡的函数被称为“动作(Actions)”。

  • 挂起状态:Actions 提供一个挂起状态,它从请求开始并在最终状态更新提交时自动重置。

  • 乐观更新:Actions 支持新的 useOptimistic 钩子,以便在请求提交时向用户显示即时反馈。

  • 错误处理:Actions 提供错误处理功能,可以在请求失败时显示错误边界,并将乐观更新自动恢复到其原始值。

  • 表单:<form> 元素现在支持向 action 和 formAction 属性传递函数。默认情况下,向 action 属性传递函数使用 Actions 并在提交后自动重置表单。

基于 Actions,React 19 引入了 useOptimistic 用于管理乐观更新,以及一个新的钩子 useActionState 用于处理 Actions 的常见情况。在 react-dom 中,我们增加了 <form> 动作来自动管理表单,并引入了 useFormStatus 以支持 Actions 在表单中的常见情况。

在 React 19 中,上述示例可以简化为:

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}>更新</button>
      {error && <p>{error}</p>}
    </form>
  );
}

新钩子:useActionState

为了使 Actions 的常见情况更简单,我们增加了一个新的钩子 useActionState

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

    // 处理成功
    return null;
  },
  null,
);

useActionState 接受一个函数(“动作”),并返回一个包裹的动作以供调用。当调用这个包裹的动作时,useActionState 会返回动作的最后结果作为数据,并返回动作的挂起状态作为挂起状态。

React.useActionState 之前在 Canary 版本中被称为 ReactDOM.useFormState,但我们已经重命名并废弃了 useFormState

React DOM:<form> 动作

Actions 还与 React 19 的新 <form> 功能集成在一起。我们增加了对在 <form><input> 和 <button> 元素的 action 和 formAction 属性中传递函数的支持,以使用 Actions 自动提交表单:

<form action={actionFunction}>

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

更多信息请参阅 react-dom 的 <form><input> 和 <button> 文档。

React DOM:新钩子 useFormStatus

在设计系统中,常见的做法是编写设计组件,而无需将属性逐层传递到组件,这些组件需要访问其所在的 <form> 的信息。虽然可以通过上下文完成,但为了使常见情况更简单,我们增加了一个新钩子 useFormStatus

import { useFormStatus } from 'react-dom';

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

useFormStatus 读取父 <form> 的状态,就像表单是一个 Context 提供者一样。

新钩子:useOptimistic

在执行数据变更时,显示乐观更新后的最终状态是另一种常见的 UI 模式。在 React 19 中,我们增加了一个新钩子 useOptimistic 来简化这一过程:

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>你的名字是: {optimisticName}</p>
      <p>
        <label>更改名字:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

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

新 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 }) {
  // 当 `Comments` 组件中的 `use` 暂停时,
  // 这个 Suspense 边界将被显示。
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

use 不支持在渲染过程中创建的 Promise。如果你试图将渲染中创建的 Promise 传递给 use,React 将发出警告:

要解决这个问题,你需要通过支持缓存 Promise 的 suspense 库或框架传递 Promise。未来我们计划推出功能,让在渲染过程中更容易缓存 Promise。

你还可以使用 use 读取上下文,从而在早期返回后有条件地读取 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 只能在渲染中调用,类似于钩子。与钩子不同,use 可以有条件地调用。未来我们计划支持更多方式在渲染时使用 use 消费资源。

新的 React DOM 静态 API

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

  • prerender

  • prerenderToNodeStream

这些新 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 Server Components

服务器组件是一种新选项,允许在打包前提前渲染组件,在独立于客户端应用程序或服务器端渲染服务器的环境中进行。这种独立环境称为“服务器”,即 React 服务器组件中的“服务器”。服务器组件可以在构建时在 CI 服务器上运行一次,或者可以使用网络服务器为每个请求运行。

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

关于如何构建服务器组件支持:

虽然 React 19 中的服务器组件是稳定的,在主要版本之间不会中断,但用于实现 React 服务器组件打包器或框架的底层 API 不遵循 semver,在 React 19.x 的小更新中可能会中断。

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

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

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

服务器组件没有指令:

一个常见的误解是服务器组件通过 "use server" 来表示,但实际上没有服务器组件的指令。"use server" 指令是用于服务器操作的。

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

React 19 的改进

ref 作为属性

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

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

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

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

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

水合错误的差异

我们还改进了 react-dom 中干预错误的错误报告。例如,之前在开发环境中记录多个没有匹配错误信息的错误,现在我们记录一个包含不匹配差异的单一消息。

<Context>作为 provider

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

const ThemeContext = createContext('');

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

新上下文提供者可以使用 <Context>,并且我们将发布一个 codemod 来转换现有的提供者。在未来的版本中,我们将弃用 <Context.Provider>

refs 的清理函数

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

<input
  ref={(ref) => {
    // ref 已创建

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

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

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

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

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

原始代码返回 HTMLDivElement 的实例,并且 TypeScript 无法知道这是清理函数还是你不想返回清理函数。

你可以使用 no-implicit-ref-callback-return codemod 来处理这种模式。

useDeferredValue 初始值

我们增加了 useDeferredValue 的初始值选项:

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

当提供了 initialValue 时,useDeferredValue 将在组件的初始渲染中返回 value,并在后台调度使用 deferredValue 返回的重新渲染。

文档元数据的支持

在 HTML 中,文档元数据标签如 <title><link> 和 <meta> 是保留用于放置在文档的 <head> 部分的。在 React 中,决定适当元数据的组件可能与渲染 <head> 的位置相距甚远,或者根本不渲染 <head>。过去,这些元素需要手动插入效果,或者使用像 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>
        E=mc²...
      </p>
    </article>
  );
}

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

样式表的支持

样式表,包括外部链接的(<link rel="stylesheet" href="...">)和内联的(<style>...</style>),由于样式优先级规则,在 DOM 中需要谨慎定位。构建允许在组件内组合的样式表功能很难,因此用户通常会加载距离组件较远的所有样式,或者使用封装这种复杂性的样式库。

在 React 19 中,我们解决了这一复杂性,并通过内置对样式表的支持,提供更深入的客户端并发渲染和服务器流式渲染集成。如果你告诉 React 样式表的优先级,它将管理样式表在 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 将确保样式表在客户端插入到 <head> 中,然后才显示依赖该样式表的 Suspense 边界内容。

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

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

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

样式库和打包器的样式集成也可以采用这一新功能,因此即使你不直接渲染自己的样式表,你也可以随着工具升级使用这个功能而受益。

异步脚本的支持

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

在 React 19 中,我们通过允许你在组件树中的任何位置渲染异步脚本来提供对异步脚本更好的支持,位于实际依赖脚本的组件内,无需管理脚本实例的重新定位和重复。

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

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

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

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

预加载资源的支持

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

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>
    <!-- 链接/脚本根据其对早期加载的实用性优先排序,而不是调用顺序 -->
    <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 可以通过将字体等额外资源的发现移出样式表加载来优化初始页面加载。它们还可以通过预取预期导航所使用的资源列表并在点击或悬停时立即预加载这些资源,使客户端更新更快。

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

我们改进了干预机制,以适应第三方脚本和浏览器扩展。

在进行干预时,如果客户端渲染的元素与从服务器接收到的 HTML 中的元素不匹配,React 会强制客户端重新渲染以修复内容。之前,如果是由第三方脚本或浏览器扩展插入的元素,会触发不匹配错误并导致客户端渲染。

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

更好的错误报告

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

这会导致每个捕获的错误有三个错误信息,在 React 19 中,我们记录一个包含所有错误信息的单一错误。此外,我们添加了两个新 root 选项来补充 onRecoverableError

  • onCaughtError:当 React 在 Error Boundary 中捕获错误时调用。

  • onUncaughtError:当错误被抛出且未被 Error Boundary 捕获时调用。

  • onRecoverableError:当错误被抛出并自动恢复时调用。

自定义元素的支持

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

在以前的版本中,由于 React 将未识别的属性视为属性而不是属性,因此在 React 中使用自定义元素是困难的。在 React 19 中,我们新增了对属性的支持,该支持在客户端和服务器端渲染期间均有效,策略如下:

  • 服务器端渲染:传递给自定义元素的 props,如果其类型是原始值(例如字符串、数字或值为 true),将呈现为属性。具有非原始类型(例如对象、符号、函数或值为 false)的 props 将被省略。

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

推荐阅读
(点击标题可跳转阅读)
[极客前沿]-你不知道的 React 18 新特性
[极客前沿]-写给前端的 K8s 上手指南
[极客前沿]-写给前端的Docker上手指南
[面试必问]-你不知道的 React Hooks 那些糟心事
[面试必问]-一文彻底搞懂 React 调度机制原理
[面试必问]-一文彻底搞懂 React 合成事件原理
[面试必问]-全网最简单的React Hooks源码解析
[面试必问]-一文掌握 Webpack 编译流程
[面试必问]-全网最全 React16.0-16.8 特性总结
[架构分享]- 微前端qiankun+docker+nginx自动化部署[自我提升]-送给React开发者十九条性能优化建议
[大前端之路]-连前端都看得懂的《Nginx 入门指南》
[软实力提升]-金三银四,如何写一份面试官心中的简历


觉得本文对你有帮助?请分享给更多人
关注「React中文社区」加星标,每天进步

“在看和转发”就是最大的支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值