来了来了~ React 19 & React Compiler (前身React forget) 它来了!

957 阅读10分钟

前言    React 版本回顾

React 上一次发布版本还要追溯到2022年6月14日,版本号是18.2.0。简单回顾一下React 18 的新特性:

  • 批量更新:将多次state更新合并处理,最终只进行一次渲染(批处理)
  • 自动批处理-Automatic Batching 
  • 对于 Promise,setTimeout、等基础异步任务中的多个 state 变更,进行自动合并。

    • 对于基于 createRoot 创建出来的组件,其下的所有状态都会应用批量更新。
  • 内存管理的增强

React18发布距今已近2年,社区也非常期待新版本的到来,而现在React19即将发布(正式发布时间暂未公布)。但在Canary 已经可以发现一些重磅更新,预告着React19即将推出的新特性,例如:use、useOptimistic hook,use client、use server 指令。这些更新在客观上丰富了 React 生态系统,特别是推动了 Next.js 和 Remix 等全栈框架的高速发展。

"Canary" 版本指的是Canary Channel、Canary Build 或Canary Release,它通常是指一种高频率更新且可能包含最新(但未经充分测试)功能和改进的版本。

React Compile(编译器)

在编译器出现以前,我们经常使用 useMemo、useCallback 和 memo 这种hook来手动缓存状态,以减少不必要的重新渲染(re-render)。此类方法虽然可行,但开发者在使用过程中学习成本相对较高 ,在项目越来越庞大的时候,开发者去识别到底要放哪些依赖项其实也是相当耗时且困难的,这也导致错误地应用不在少数,而错误的使用可能会导致整体效率更加低下。故 React 团队认为这并不是他们理想中的性能优化方式。

而在今年,React Compiler 正式开源了(前身 React Forget)。这是一个用Rust编写的"自动记忆编译器",利用对 JavaScript 和 React 规则的掌握,编译器能够解析React代码且自动记忆组件和钩子中的值或值组,且自动识别到真正可以提升性能的依赖项并处理,确保代码的高效运行。

需要明确的是,React 19 和 React 编译器是两件不同的事物。React 团队在他们宣布即将发布 React 19 的同一篇博文中提到了编译器,这让许多人误以为二者是相同的,误解纷纷产生(笔者一开始也误会了)。后来React团队的一位成员通过一条推文对这个误解进行了说明↓(图片转载自掘金社区)。

目前编译器已经支持单独接入(但最好是升级至React19后再接入),有兴趣的同学可以跟着官方文档接入试试。但编译器目前仍处于实验阶段,但不建议投入生产,可以让子弹再飞一会,等稳定版本公布再行动。

对React Compiler原理有兴趣的铁子建议看2024-05 React Conf 2024 大会上各大神的分享,链接奉上:zh-hans.react.dev/blog/2024/0…

React19 新特性分享

1. Action & 相关hook

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

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>
  );
}

Action的产生,可以简化过往繁琐的表单处理各步骤,且与 React 19 的 react-dom 的新 <form> 功能集成。添加了对以下方面的支持:

  1. 将函数作为 <form><input> 的 action 和 formAction 属性传递:

action 函数是可以同步或异步操作。使用 action 时,React 将为开发者管理数据提交的生命周期,同时我们还可以通过 useFormStatus 和 useActionState 这两个 hook 来访问表单操作的当前状态和响应(详细见下文hook介绍)。

  1. <button> 元素自动提交带有操作的表单:

<form> 元素目前支持将函数传递给 action 和 formAction 属性。将函数传递给 action 属性默认使用 Actions,并在提交后自动重置表单。(如果需要手动重置 

,可以调用新的 requestFormReset React DOM API。)

<form action={search}>
  <input name="query" />
  <button type="submit">Search</button>
</form>

1.1. hook update:useTransition 

useTransition不需要接受任何参数,返回一个由两个元素组成的数组:

  1. isPending,告诉你是否存在待处理的 transition。
  2. startTransition 函数,你可以使用此方法将状态更新标记为 transition。

useTransition其实在React18中就已经出现,但仅对同步函数做了支持。在 React 19 中,useTransition 添加了对在转换中使用异步函数的支持,以自动处理挂起状态、错误、表单和乐观更新,可视为一种loading响应。

// Using pending state from Actions
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>
  );
}

上述代码运行结果实际是一个表单,元素包括一个Input输入框和一个提交按钮,提交回调(handleSubmit)实际是一个被startTransition包裹的异步函数。当按钮被触发点击,异步转换会将立即将 isPending 状态设置为 true,发出异步请求,并在完成异步请求后将 isPending 切回为 false。这使您可以在数据更改时保持当前 UI 的响应能力和交互性。

1.2. new hook:useOptimistic 来管理乐观更新

useOptimistic 可用于在异步操作(如网络请求)进行时,乐观地更新 UI。

在下面的case中,useOptimistic 通过接受一个当前状态(currentName),返回一个在异步操作期间可能会有所不同的状态副本和一个更新函数。向form提供一个submitAction函数,这个函数在接收表单的输入时,先把状态副本更新,这样在页面上看到的就是最新的状态,在异步请求完成后再更新真实的状态,从而达到乐观更新的效果。

乐观更新:是一种在前端开发中常用的处理异步操作反馈的策略。它基于一种“乐观”的假设:即假设无论我们向服务器发送什么请求,这些操作都将成功执行,因此在得到服务器响应之前,我们就提前在用户界面上渲染这些改变。使用场景:点赞、评论、任务添加编辑等。

function ChangeName({currentName, onUpdateName}) {
  // currentName:当前状态; setOptimisticName:更新函数
  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>
  );
}

1.3. new hook:useActionState 来处理Actions的常见情况

useActionState 接受一个函数(即 Action),并返回一个被包装过的 Action (submitAction)供 form调用。当调用submitAction 时,useActionState 将返回 Action 的最后结果作为 data,并将 Action 的挂起状态作为 pending。

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.useActionState 之前在 Canary 版本中称为 ReactDOM.useFormState,但我们已重命名它并弃用 useFormState。)

1.4. new hook:useFormStatus 表单状态获取

useFormStatus 读取父级  的状态,就好像该表单是上下文提供者一样。注意:仅返回父级form状态,同一组件内使用无效。

import {useFormStatus} from 'react-dom';

function DesignButton() {
  // 父级 <form> 的状态:const { pending, data, method, action } = useFormStatus();
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

错误示范:

更多 : useFormStatus – React

1.5. 小结

了解完以上内容后,相信大家对React19中的Action及相关hook已经有初步的了解。可预见,未来Action使用场景广泛,可以在执行数据库变更(如增加、删除、更新数据)和实现表单(如登录表单、注册表单)等客户端到服务器交互的场景中使用。

2. 新API :use

  • use API只能在render中调用,类似于hooks,但与 hook 不同,use 可以有条件地调用。
  • useMemo、useCallback 可用 use 代替,无需考虑复杂的依赖项依赖关系,react智能化更高,自动识别。

下面举两个use使用的🌰:

2.1. use 来读取渲染中的资源

例如,使用 use 读取 Promise,React 将 Suspend 直到 Promise 解析:

import {use} from 'react';

function Comments({commentsPromise}) {
  // `use` will suspend until the promise resolves.
  const comments = use(commentsPromise);
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // When `use` suspends in Comments,
  // this Suspense boundary will be shown.
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

tip:use 不支持在渲染中创建的 Promise。

2.2. use 读取上下文

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

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  // 当 Heading 内调用 use(ThemeContext) 时,它将收到 "dark" 作为值。
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}


// 祖先元素
function MyPage() {
  return (
    <ThemeContext.Provider value="dark">
      <Heading/>
      // 中间隔多少层组件无所谓
    </ThemeContext.Provider>
  );
}

3. Server Components(服务器组件)

服务器组件,顾名思义,就是在服务端渲染的组件,所有逻辑都在服务端(也支持编译时)执行,包括数据获取、状态管理等。React19中 服务器组是一种新的选项,允许在打包前提前渲染组件,存在于客户端应用程序或 SSR 服务器不同的环境中。这个独立的环境就是 React 服务器组件中的 “服务器”。服务器组件可以在你的 CI 服务器上在构建时运行一次,或者可以在每次请求时使用 web 服务器运行。

如果开发者使用 React,而不是使用全栈框架,只需要了解这两个指令的作用即可:use client  和  use server 。这两个指令分别标记客户端组件和服务器组件,也区分了前端和服务端两个环境,use client 指示打包工具生成一个 

两个指令在 Canary 版本发布已久,据说也会在本次v19版本里加入Stable版本。可以说是全面拥抱NextJs了,感兴趣的同学还可以参考:NextJS v13服务端组件和客户端组件及最佳实践

'use client'
 
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
import db from './database';
 
async function Note({ id }) {
  const note = await db.notes.get(id);
  return (
    <div>
      <Author id={note.authorId} />
      <p>{note}</p>
    </div>
  );
}
 
async function Author({ id }) {
  const author = await db.authors.get(id);
  return <span>By: {author.name}</span>;
}

4. 其他更新

  •   as a provider,无需再用 Context.Proivder 包裹节点
const ThemeContext = createContext('');

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

总结

本文介绍了React Compiler 与React19的新特性,篇幅原因还有一些内容没有全部展开,未展开内容建议大家可以前往官网文档了解更多。感谢铁子们看到这里!如本文分享有何错漏之处,欢迎各位大佬批评指正[鞠躬] 。也欢迎沟通交流!

参考文档:

1、官网发布的 React 19 Beta 文档:

react.dev/blog/2024/0…

2、react-19-playground:github.com/bradtravers… (可以clone到本地亲自尝试使用一下各种新特性噢)

3、weijunext.com/article/rea…