当前位置: 欣欣网 > 码农

React 19 终于来了!

2024-04-28码农

未经授权,禁止转载,转载请注明出处!

早在数月前,React 团队便预告了 React 19 的积极开发,并预计上半年发布。 4 月 25 日 ,React 终于发布了 v 19 测试版 。该版本主要面向各大库,以确保它们与 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 来处理挂起和错误状态:

    functionUpdateName({}){
    const[name, setName]=useState("");
    const[error, setError]=useState(null);
    const[isPending, setIsPending]=useState(false);
    consthandleSubmit=async()=>{
    setIsPending(true);
    const error =awaitupdateName(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 中增加了对在转换中使用异步函数的支持,以自动处理挂起状态、错误、表单和乐观更新。

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

    functionUpdateName({}){
    const[name, setName]=useState("");
    const[error, setError]=useState(null);
    const[isPending, startTransition]=useTransition();
    consthandleSubmit=async()=>{
    startTransition(async()=>{
    const error =awaitupdateName(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 中,上面的例子可以简化为:

    functionChangeName({ name, setName }){
    const[error, submitAction, isPending]=useActionState(
    async(previousState, formData)=>{
    const error =awaitupdateName(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 =awaitupdateName(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';
    functionDesignButton(){
    const{ pending }=useFormStatus();
    return<button type="submit" disabled={pending}/>;
    }

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

    全新 Hook:useOptimistic

    在数据变更操作中,一种常见的 UI 模式是在异步请求执行期间乐观地显示预期的最终状态。React 19 引入了新的 useOptimistic Hook,以简化这一流程:

    functionChangeName({currentName, onUpdateName}){
    const[optimisticName, setOptimisticName]=useOptimistic(currentName);
    constsubmitAction=async formData =>{
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName =awaitupdateName(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';
    functionComments({ commentsPromise }){
    // 使用use读取Promise,React会在Promise解析前挂起组件渲染
    const comments =use(commentsPromise);
    return(
    <div>
    {comments.map((comment)=>(
    <p key={comment.id}>{comment.text}</p>
    ))}
    </div>
    );
    }
    functionPage({ 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'
    functionHeading({children}){
    if(children ==null){
    returnnull;
    }
    // 使用 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 作为函数组件的参数进行访问:

    functionMyInput({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('');
    functionApp({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 选项,该选项允许指定组件首次渲染时返回的值。

    functionSearch({ deferredValue }){
    // 在组件首次渲染时,返回 initialValue 作为 value。
    // 随后,useDeferredValue 会在后台计划一次重渲染,使用 deferredValue 作为新的 value。
    const value =useDeferredValue(deferredValue,{initialValue:''});
    return(
    <Resultsquery={value}/>
    );
    }

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

    文档元数据支持

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

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

    下面是一个简单的示例,展示了如何在React组件中定义并使用这些元数据标签:

    functionBlogPost({ 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中的插入顺序,确保在展示依赖于特定样式规则的内容之前,相关的样式表(无论外部还是内嵌)已经被加载并应用。

    functionComponentOne(){
    return(
    <Suspensefallback="loading...">
    <link rel=" stylesheet"href="foo"precedence="default"/>
    <link rel=" stylesheet"href="bar"precedence="high"/>
    <article class="foo- class bar- class">
    {...}
    </article>
    </Suspense>
    )
    }
    functionComponentTwo(){
    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将确保样式表在文档中只被包含一次。

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

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

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

    异步脚本支持

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

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

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

    functionMyComponent(){
    return(
    <div>
    <script asyncsrc="..."/>
    Hello World
    </div>
    );
    }
    functionApp(){
    return(
    <html>
    <body>
    <MyComponent/>
    ...
    <MyComponent/> // 不会导致DOM中出现重复的脚本
    </body>
    </html>
    );
    }

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

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

    资源预加载支持

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

    import{ prefetchDNS, preconnect, preload, preinit }from'react-dom';
    functionMyComponent(){
    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 asyncsrc="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