React 19 Beta

原文信息: 查看原文查看原文

React 19 Beta

- The React Team

React 19 Beta 现已在 npm 上可用!在本文中,我们将概述 React 19 的新功能以及如何采用它们。

注意

此 Beta 版本是为了让库为 React 19 做准备。应用开发者应该升级到 18.3.0 并等待 React 19 稳定版,因为我们将与库合作并根据反馈进行更改。

React 19 Beta 现已在 npm 上可用!

在我们关于 React 19 Beta 升级指南 的帖子中,我们分享了将您的应用升级到 React 19 Beta 的逐步说明。在本文中,我们将概述 React 19 的新功能以及如何采用它们。

有关破坏性变更的列表,请查看 升级指南


React 19 中的新内容

Action

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

例如,您可以在 useState 中处理待处理和错误状态:

// 在Action之前  
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 来自动处理您的待处理状态:

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

  const handleSubmit = async () => {  
    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 的响应性和交互性。

注意

按惯例,使用异步转换的函数称为“Action”。

Action自动为您管理提交数据:

  • 待处理状态:Action提供一个从请求开始时开始的待处理状态,并在最终状态更新提交时自动重置。
  • 乐观更新:Action支持新的 useOptimistic 钩子,以便在提交请求时向用户显示即时反馈。
  • 错误处理:Action提供错误处理,以便在请求失败时显示错误边界,并自动将乐观更新回退到原始值。
  • 表单<form> 元素现在支持将函数传递给 actionformAction 属性。将函数传递给 action 属性默认使用Action,并在提交后自动重置表单。

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

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

// 使用 <form> Action和 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 (  
    <form action={submitAction}>  
      <input type="text" name="name" />  
      <button type="submit" disabled={isPending}>更新</button>  
      {error && <p>{error}</p>}  
    </form>  
  );  
}

在下一节中,我们将分解 React 19 中每个新Action特性。

新钩子:useActionState

为了使Action的常见情况更易于处理,我们添加了一个名为 useActionState 的新钩子:

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

  // 处理成功  
});

useActionState 接受一个函数(“Action”),并返回一个包装后的Action来调用。这是因为Action是可组合的。当调用包装后的Action时,useActionState 将返回Action的最后一个结果作为 data,并返回Action的待处理状态作为 pending

注意

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

有关更多信息,请查看 #28491

有关更多信息,请查看 useActionState 的文档。

React DOM:<form> Action

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

<form action={actionFunction}>

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

有关更多信息,请查看 react-dom 文档中的 <form><input><button>

React DOM:新钩子:useFormStatus

在设计系统中,编写需要访问其所在 <form> 的信息的设计组件是很常见的,而不必将 props 向下传递到组件。这可以通过 Context 完成,但为了使常见情况更简单,我们添加了一个新的钩子 useFormStatus

import {useFormStatus} from 'react-dom';  

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

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

有关更多信息,请查看 react-dom 文档中的 useFormStatus

新钩子: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 值。

有关更多信息,请查看 useOptimistic 的文档。

新 API:use

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

例如,您可以使用 use 读取一个承诺,React 将在承诺解决之前暂停:

import {use} from 'react';  

function Comments({commentsPromise}) {  
  // `use` 将在承诺解决之前暂停。  
  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 不支持在渲染中创建的承诺。

如果您尝试将渲染中创建的承诺传递给 use,React 将警告:

要修复它,您需要从支持承诺缓存的 Suspense 支持库或框架传递一个承诺。我们计划在未来推出功能,使在渲染中缓存承诺更容易。

您也可以使用 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 只能在渲染中调用,类似于钩子。与钩子不同,use 可以有条件地调用。我们计划在未来支持使用 use 的更多方式来在渲染中消费资源。

有关更多信息,请查看 use 的文档。

React Server Components

服务器组件

服务器组件是一个新的选项,允许在构建之前、捆绑之前,在与您的客户端应用程序或 SSR 服务器不同的环境里提前渲染组件。这个独立环境就是 React 服务器组件中的“服务器”。服务器组件可以在 CI 服务器上的构建时运行一次,也可以在每个请求中使用 Web 服务器运行。

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

注意

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

虽然 React 19 中的 React 服务器组件是稳定的,并且在主要版本之间不会破坏,但用于实现 React 服务器组件捆绑器或框架的底层 API 不遵循 semver,并且在 React 19.x 的次要版本之间可能会破坏。

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

有关更多信息,请查看 React 服务器组件 的文档。

服务器Action

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

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

注意

没有用于服务器组件的指令。

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

有关更多信息,请查看 指令 的文档。

服务器Action可以在服务器组件中创建,并作为 props 传递给客户端组件,或者它们可以被导入并在客户端组件中使用。

有关更多信息,请查看 React 服务器Action 的文档。

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 不是作为 prop 传递的,因为它们引用组件实例。

水合错误的 Diffs

我们还改进了 react-dom 中水合错误的错误报告。例如,以前在 DEV 中记录多个错误而没有任何关于不匹配的信息:

我们现在记录一个带有不匹配差异的消息:

<Context> 作为提供程序

在 React 19 中,您可以将 <Context> 作为提供程序而不是 <Context.Provider> 渲染:

const ThemeContext = createContext('');  

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

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

ref 的清理函数

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

<input  
  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 来 codemod 这个模式。

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 等于 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>  
    <!-- 链接/脚本根据其对早期加载的实用性进行优先级排序,而不是调用顺序 -->  
    <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

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

我们已经改进了水合,以考虑第三方脚本和浏览器扩展。

在水合时,如果在客户端呈现的元素与服务器 HTML 中找到的元素不匹配,React 将强制客户端重新渲染以修正内容。以前,如果元素是由第三方脚本或浏览器扩展插入的,它将触发不匹配错误并客户端呈现。

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

更好的错误报告

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

这导致每次捕获到的错误都会记录三次错误:

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

此外,我们还为 onRecoverableError 添加了两个新的根选项:

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

有关更多信息和示例,请查看 createRoothydrateRoot 的文档。

支持自定义元素

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

在过去的版本中,由于 React 将未识别的 props 视为属性而不是属性,使用 React 中的自定义元素一直很困难。在 React 19 中,我们添加了对属性的支持,并在客户端和 SSR 上使用以下策略:

  • 服务器端渲染:传递给自定义元素的 props 将作为属性呈现,如果它们的类型是原始值,如 stringnumber,或者值是 true。具有非原始类型 props,如 objectsymbolfunction,或者值是 false 将被省略。
  • 客户端渲染:匹配自定义元素实例上的属性的 props 将被分配为属性,否则它们将被分配为属性。

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

如何升级

请查看 React 19 升级指南 以获取逐步说明和完整列表的破坏性和值得注意的更改。

分享于 2024-04-28

访问量 10

预览图片