• React19测试版发布 快来看下都有哪些新特性
  • 发布于 1周前
  • 63 热度
    0 评论

早在数月前,React 团队便预告了 React 19 的积极开发,并预计上半年发布。 4 月 25 日,React 终于发布了 v19 测试版。该版本主要面向各大库,以确保它们与 React 19 的兼容性。因此,建议开发者先升级至最新的稳定版 18.3.0,静待 React 19 的正式版发布。


React 19 带来了诸多新特性和改进,不仅提升了开发者的使用体验,还进一步优化了 React 应用的性能。为了让开发者能够平稳过渡到 React 19,React 团队特意准备了一份详尽的升级指南,详细列出了升级步骤和可能遇到的重大变化。接下来,本文将剖析 React 18.3 与 React 19 的更新内容,并探索 React 19 的升级指南,助力更好地拥抱这一重大更新!

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

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

React DOM
.对使用已废弃的 unmountComponentAtNode 方法时发出警告。
.对使用已废弃的 renderToStaticNodeStream 方法时发出警告。

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");
  };
  // 堆代码 duidaima.com
  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 中增加了对在转换中使用异步函数的支持,以自动处理挂起状态、错误、表单和乐观更新。例如,可以使用useTransition来自动处理挂起状态:
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}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}
异步转换会立刻将isPending状态设为true,发起异步请求,并在转换完成后将isPending设置为false。这样可以确保数据变化时,UI 仍然保持响应和交互性。在 Actions 功能的基础上,React 19 还推出了useOptimistic Hook 来简化乐观更新的管理,以及 React.useActionState Hook来处理 Actions 的常见场景。同时,在 react-dom 中,新增了<form> Actions 来自动管理表单操作,并提供了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 (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}
下面就来看看这些新的 Actions 功能的作用和使用方法。

全新 Hook:useActionState
为了让常见的 Action 用例更加简便,因此添加了一个名为useActionState的新 Hook:
const [error, submitAction, isPending] = useActionState(async (previousState, newName) => {  
  const error = await updateName(newName);  
  
  if (error) {  
    // 可以返回Action的任何结果,这里只返回错误信息。  
    return error;  
  }  
  
  // 处理成功的情况  
});
useActionState接受一个函数(即“Action”),并返回一个可调用的包装 Action。这之所以能够工作是因为Actions是可组合的。当调用包装后的Action时,useActionState将返回Action的最后一个结果作为数据,并将Action的挂起状态作为pending返回。

React DOM: <form> Actions
在 React 19 中,Actions 与 react-dom 的新 <form> 特性进行了深度整合。现在,可以将函数作为<form>、<input>和<button>元素的action和formAction属性,以便自动使用 Actions 提交表单:
<form action={actionFunction}>
当<form>的action成功执行时,React会自动为不受控组件重置表单状态。如果需要手动重置<form>,React DOM API 提供了全新的requestFormReset方法。

React DOM 新 Hook:useFormStatus
在构建设计系统时,经常需要创建一些设计组件,这些组件需要能够访问其所在表单的状态信息,而不必通过层层传递props来实现。虽然通过Context也可以实现这一功能,但为了简化常见场景下的使用,React 19 引入了一个新的 Hook useFormStatus:
import { useFormStatus } from 'react-dom';  
function DesignButton() {  
  const { pending } = useFormStatus();  
  
  return <button type="submit" disabled={pending} />;  
}
useFormStatus可以读取父级<form>的状态,就像该表单是一个Contextprovider 一样。

全新 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请求还在进行时,立即将输入框的值(即newName)设置为optimisticName,从而乐观地更新UI。如果更新成功,则通过onUpdateName回调函数来确认状态的更改;如果更新失败或发生错误,可以通过setOptimisticName回滚到原始状态currentName。

全新 API:use
在React 19中,引入了一个全新的API——use,它允许在组件渲染时直接读取资源。举个例子,可以使用use来读取一个Promise对象,而React将会自动挂起(Suspend)组件的渲染,直到该Promise解析完成:
import { use } from 'react';  
  
function Comments({ commentsPromise }) {  
  // 使用use读取Promise,React会在Promise解析前挂起组件渲染  
  const comments = use(commentsPromise);  
  
  return (  
    <div>  
      {comments.map((comment) => (  
        <p key={comment.id}>{comment.text}</p>  
      ))}  
    </div>  
  );  
}  
  
function Page({ commentsPromise }) {  
  // 当Comments组件因use挂起时,这里会显示Suspense的fallback内容  
  return (  
    <Suspense fallback={<div>加载中...</div>}>  
      <Comments commentsPromise={commentsPromise} />  
    </Suspense>  
  );  
}
同样可以使用use来读取上下文(Context),从而实现在特定条件下(例如早期返回之后)按需读取上下文数据。早期返回指的是在函数或方法中的某个点提前结束执行,并返回结果或退出,而不是继续执行剩余的代码。在函数内部,根据某些条件或逻辑,你可能会决定不需要继续执行后续的代码,此时可以使用return语句来立即退出函数。
import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  // 堆代码 duidaima.com
  // 使用 useContext 在这里不会生效 ,因为存在早期返回。
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}
use API 的调用仅限于渲染阶段,与 React 的 Hook 类似。然而,与 Hook 不同的是,use API 允许在条件语句中灵活调用。展望未来,React 团队计划进一步扩展 use API 的功能,提供更多在渲染时消费资源的方式。

React 服务器组件
服务器组件
服务器组件是一种创新性的技术,它允许在打包前,在独立于客户端应用程序或服务器端渲染(SSR)服务器的环境中预先渲染组件。这个独立的环境即为React服务器组件中的“服务器”。服务器组件有两种运行模式:一种是在构建时(例如在持续集成服务器上)运行一次;另一种是针对每个请求,通过 Web 服务器进行实时运行。

React 19 全面集成了来自 Canary 频道的所有服务器组件特性。这意味着,那些带有服务器组件的库现在可以将 React 19 作为对等依赖项进行目标定位,并通过 react-server 导出条件,为支持全栈 React 架构的框架提供强大的支持。

服务器操作
服务器动作是一项强大功能,它允许客户端组件调用并执行在服务端的异步函数。通过“use server”指令定义服务器操作后,框架将智能地创建一个指向服务端函数的引用,并安全地将该引用传递给客户端组件。当客户端组件需要调用这个函数时,React 会负责向服务器发送请求,执行相应的函数,并将结果返回给客户端。

服务器操作的创建非常灵活,既可以在服务器组件内部创建,并作为属性传递给客户端组件使用;也可以直接在客户端组件中导入并调用。这种设计使得服务器操作能够无缝集成到应用中,实现前后端数据的流畅交互。

功能改进
ref 作为属性
从 React 19 开始,可以将 ref 作为函数组件的参数进行访问:
function MyInput({placeholder, ref}) {  
  return <input placeholder={placeholder} ref={ref} />  
}  
  
//...  
  
<MyInput ref={ref} />
新的函数组件将不再需要 forwardRef,React 团队将会发布一个代码转换工具来自动更新组件,以使用新的 ref 属性。在未来的版本中,将弃用并移除 forwardRef。

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

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

作为提供者
React 19 允许直接将 <Context> 用作提供者,而无需使用传统的 <Context.Provider> 写法:
const ThemeContext = createContext('');  
  
function App({children}) {  
  return (  
    <ThemeContext value="dark">  
      {children}  
    </ThemeContext>  
  );  
}
这种新的语法更加简洁直观。为了方便开发者升级现有代码,React 团队将发布一个代码转换工具,能够自动将现有的 <Context.Provider> 转换为新的 <Context> 提供者。未来版本中,将逐步弃用 <Context.Provider>,以推动 React 社区向更加简化的语法过渡。

refs 清理函数
现在支持从 ref 回调函数中返回一个清理函数:
<input  
  ref={(ref) => {  
    // 创建 ref  
  
    // 新增:返回一个清理函数,当元素从 DOM 中移除时重置 ref。  
    return () => {  
      // ref 的清理工作  
    };  
  }}  
/>
当组件卸载时,React 将调用从 ref 回调函数中返回的清理函数。这适用于 DOM refs、类组件的 refs 以及 useImperativeHandle。由于引入了 ref 清理函数的机制,现在 TypeScript 将拒绝从 ref 回调函数中返回除清理函数以外的任何内容。为了避免这个问题,我们通常建议避免使用隐式返回,比如将赋值操作放在花括号中,如下所示:

原来的写法:
<div ref={current => (instance = current)} />
优化后的写法:
<div ref={current => { instance = current; }} />
这种改变是因为 TypeScript 无法判断原始代码中返回的是否应该是清理函数,还是无意中的隐式返回值。通过将赋值操作明确地包裹在花括号中,确保了 ref 回调中不会意外地返回任何值,除非有意为之。为了自动化这种模式的转换,可以使用 no-implicit-ref-callback-return 规则进行代码转换。这将帮助你在升级 React 版本时更顺畅地处理 ref 相关的代码。

useDeferredValue 的初始值
React 19 为 useDeferredValue 引入了 initialValue 选项,该选项允许指定组件首次渲染时返回的值。
function Search({ deferredValue }) {  
  // 在组件首次渲染时,返回 initialValue 作为 value。  
  // 随后,useDeferredValue 会在后台计划一次重渲染,使用 deferredValue 作为新的 value。  
  const value = useDeferredValue(deferredValue, { initialValue: '' });  
  
  return (  
    <Results query={value} />  
  );  
}
使用 initialValue 可以确保组件在首次渲染时能够立即显示一个占位值,而无需等待 deferredValue 的异步计算完成。随后,当 deferredValue 准备好时,useDeferredValue 会触发组件的后台重渲染,以显示最新的值。这有助于提升应用的响应性和用户体验。

文档元数据支持
在HTML中,诸如<title>、<link>和<meta>等文档元数据标签通常放置在<head>区域内。然而,在React应用中,决定哪些元数据适用于当前页面的组件可能并不直接位于渲染<head>的位置,甚至React可能根本不直接渲染<head>。过去,这些元数据标签需要手动通过effect或借助如react-helmet这样的库进行插入,且在服务器渲染React应用时需要特别小心处理。

React 19引入了原生支持在组件中渲染文档元数据标签的功能。这意味着可以直接在组件内部定义这些标签,React会自动将它们提升到文档的<head>部分。这一改进确保了元数据标签能够在仅客户端应用、流式服务端渲染(SSR)以及服务器组件等场景下正常工作。

下面是一个简单的示例,展示了如何在React组件中定义并使用这些元数据标签:
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.join(', ')} />  
      <p>  
        Eee equals em-see-squared...  
      </p>  
    </article>  
  );  
}
在这个BlogPost组件中,尽管<title>、<meta>和<link>标签被定义在了<article>内部,但React会在渲染时自动将它们移至文档的<head>区域。这种方式简化了元数据的管理,还增强了React应用在各种渲染模式下的兼容性和灵活性。

样式表支持
在Web开发中,样式表的管理至关重要,无论是通过外部链接(<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" />  <-- will be inserted between foo & bar
    </div>
  )
}
在服务端渲染过程中,React会将样式表包含在<head>标签中,以确保浏览器在加载完成前不会进行页面绘制。如果在已经开始流式传输后才发现样式表,React 将确保在客户端将样式表插入到<head>标签中,然后再展示依赖于该样式表的Suspense边界的内容。

在客户端渲染过程中,React会等待新渲染的样式表加载完成后再提交渲染结果。如果在应用中的多个位置渲染了这个组件,React将确保样式表在文档中只被包含一次。
function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // 不会导致 DOM 中出现重复的样式表链接
  </>
}
对于那些习惯于手动加载样式表的开发者来说,React 19 的这一改进为他们提供了一个便利的机会。现在,可以将样式表直接放在依赖它们的组件旁边,这不仅有助于提升代码的可读性和可维护性,使得开发者可以更加清晰地了解每个组件的样式依赖关系,而且还能够确保只加载真正需要的样式表。

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

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

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

现在,你可以在组件中这样使用异步脚本:
function MyComponent() {  
  return (  
    <div>  
      <script async 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://...'); // 当确定会从该主机请求资源但不确定具体资源时  
}
这些 API 调用会在渲染组件时生成相应的DOM标签,如下所示:
<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都为开发者提供了强大的工具,使资源加载更加智能和高效。

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

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

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

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

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

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

此外,为了提供更细粒度的错误处理控制,还新增了两个根选项来与onRecoverableError相辅相成:
onCaughtError:当React在错误边界中成功捕获到错误时,此选项将被调用。
onUncaughtError:当错误被抛出且未能被任何错误边界捕获时,此选项将被触发。
onRecoverableError:当错误发生但React能够自动恢复时,该选项将起作用。

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

自定义元素支持
React 19 现已正式加入对自定义元素的全方位支持,并成功通过 Custom Elements Everywhere 的所有测试。在过去,React 对于自定义元素的处理并不尽如人意,因为它往往将未识别的属性(props)默认为属性(attributes)而非属性(properties)。然而,在 React 19 中增加了对属性的支持,这种支持既适用于客户端也适用于服务器端渲染(SSR),具体策略如下:

在服务端渲染时,如果传递给自定义元素的属性(props)是基本数据类型(如字符串、数字或值为true),它们将作为属性(attributes)进行渲染。相反,如果属性的类型是非基本类型(如对象、符号、函数或值为false),它们将不会被渲染,确保了数据的正确性和一致性。

在客户端渲染时,React 19 则更加智能地处理属性。如果属性(props)与自定义元素实例上的属性(properties)相匹配,它们将被直接设置为属性(properties)。否则,它们将作为属性(attributes)进行分配。这种处理方式不仅提升了性能,还使得React在处理自定义元素时更加准确和高效。

React 19 升级指南
React 19 的升级带来了一系列重要的改进,尽管其中涉及到一些破坏性变更,但 React 团队已经努力使升级过程变得更顺畅,预计对大多数应用的影响有限。

注意: 此 beta 版的主要目的是帮助库开发者为即将到来的React 19做好准备。对于应用开发者而言,建议先升级到React 18.3.0 版本,并耐心等待 React 19 稳定版的发布。React 团队正与库开发者紧密合作,并根据他们的反馈进行必要的调整和改进。为了降低升级难度,React 同步推出了 18.3 版本,以帮助开发者更好地适应未来的变更。
React 19 升级指南:https://react.dev/blog/2024/04/25/react-19-upgrade-guide
用户评论