React 19 终于发布了

796 阅读9分钟

前沿

昨晚(2024-12-05),React19版本终于发布了,我想苦逼的前端程序员又要开始版本升级了,升级固然是有成本的,但要权衡利弊,根据团队资源分布和项目稳定性方面考虑是否要对旧项目做版本升级。

下面跟着官方文档,介绍一下本次升级内容。

19版本文档地址:19.react.dev/ 19版本更新文档:19.react.dev/blog/2024/1…

新特性介绍

useActionState

顾名思义,行为状态,比以前的useState多了一个action,说明这个状态跟UI的行为关联起来了。

需求: 一个文本框,一个按钮,监听文本输入,点击按钮提交进行跳转。这个过程需要判断提交成功和失败以及请求过程中需Loading效果。

老版本使用方式:

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

说明:在过去我们是分别定义state来存储请求过程的各个状态,用来呈现在页面中。

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

咋一看没什么变化,useTransition是过去的版本,在React19中只是增加了对异步函数的支持,你可以在里面使用await做同步调用。

还有一个改动是乐观更新,什么意思呢?当你使用useTransition时,isPending立马为true,按钮就开始Loading,只要有任何状态转换,isPending就变为false,也可以理解为先假设这个过程是成功的,前端优先响应成功的UI交互,这就是乐观更新。

新版本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 null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

很明显这种方式更加简单,有点像自定义HookuseActionState直接返回错误信息、提交动作和isPending状态,一个Hook就全搞定了。

语法介绍:

const [state,action,isPending] = useActionState(action, initialState, permalink?)

数组返回:

  • state 返回的新状态
  • action 执行的行为函数
  • isPending 执行行为的过程对应的状态,初始化为true,状态变化后为false

参数介绍

  • action: 是一个函数,接收状态变化前后的值。
  • initialState: 初始化的状态值。
  • permalink:可选永久链接,我还不清楚在项目中如何运用。

示例:

import { useActionState } from "react";

async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm({}) {
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  )
}
  • useActionState初始化状态值为0
  • 点击按钮触发formAction,执行increment函数,返回新状态值。
  • state 最终变成1,回显在页面。
  • 这个例子中由于没有调用接口,所以没有添加isPending状态。

这个例子是非常简单而清楚的,那我们在什么场景下用呢?多个状态的组合,比如点击按钮显示Loading,调用接口成功,关闭Loading,回显状态等,这种场景就非常适合。

useFormStatus

const { pending, data, method, action } = useFormStatus();

这是一个表单提交状态的Hook,它在什么场景下用呢?我先给大家举一个例子:

import { useFormStatus } from "react-dom";

// 提交组件
function Submit() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

export default function Form({ action }) {
  async function action(query) {
      await new Promise((res) => setTimeout(res, 1000));
  }
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

这个例子非常简单,就是form组件下嵌套了一个提交组件,在提交组件内使用useFormStatus,它可以接收到当前form表单的提交状态,点击按钮时,在接口没有返回前pendingtrue,返回以后,pending变为false,它可以有效避免重复点击。

需要注意的是,这个状态的使用必须放在独立的组件里面,不能直接嵌套在form下,比如下面这样:

export default function Form({ action }) {
  const { pending } = useFormStatus();
  async function action(query) {
      await new Promise((res) => setTimeout(res, 1000));
  }
  return (
    <form action={action}>
      <button type="submit" disabled={pending}>
        {pending ? "Submitting..." : "Submit"}
      </button>
    </form>
  );
}

如果你这样用的话,pending始终为false,所以你需要把它放在form组件的外面,才能用来接收form的各个状态,比如:pendingdatamethodaction等。

再看一个例子

import UsernameForm from './UsernameForm';
import {useRef} from 'react';

function UsernameForm() {
  const {pending, data} = useFormStatus();

  return (
    <div>
      <h3>Request a Username: </h3>
      <input type="text" name="username" disabled={pending}/>
      <button type="submit" disabled={pending}>
        Submit
      </button>
      <br />
      <p>{data ? `Requesting ${data?.get("username")}...`: ''}</p>
    </div>
  );
}

export default function App() {
  const ref = useRef(null);
  
  // 表单提交接口
  cosnt submitForm = ()=>{...}
  
  return (
    <form ref={ref} action={async (formData) => {
      await submitForm(formData);
      ref.current.reset();
    }}>
      <UsernameForm />
    </form>
  );
}

UsernameForm组件可以接受来自父组件form中的pendingdata数据,是不是有点像过去的useContext(),因为我们知道父子组件传递需要<Context.Provider value="123">才行,但现在你通过useFormStatus就可以直接获取到了。

useOptimistic

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

这是一个乐观更新的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 钩子将在 updateName 请求进行时立即呈现 optimisticName。当更新完成或出错时,React 将自动切换回 currentName 值。

简而言之:钱你先拿去用(这是我对你的信任),有了再还我,假如你跑了,我就去找你老婆要。

use

这是一个新的 API 来读取 render 中的资源,包含Promise,注意它不是Hook

看一下官方的例子:

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

commentsPromise是一个Promise对象,在评论组件里面(Comments)通过use可以直接读取这个Promise对象,拿到结果以后再进行遍历,返回评论列表。

它可以读什么呢?

  • Promise
const message = use(new Promise());
  • Context
const theme = use(ThemeContext);

特别注意:与 React Hook 不同,use 可以在循环和条件语句(如 if)中调用。与 React Hook 一样,调用 use 的函数必须是 ComponentHook

比如:在if里面可以用use,但不可以用useState

function HorizontalRule({ show }) {
  if (show) {
    const theme = use(ThemeContext);
    return <hr className={theme} />;
  }
  return false;
}

看一个完整的例子:

import { createContext, use } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

function Form() {
  return (
    <Panel title="Welcome">
      <Button show={true}>Sign up</Button>
      <Button show={false}>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = use(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ show, children }) {
  if (show) {
    const theme = use(ThemeContext);
    const className = 'button-' + theme;
    return (
      <button className={className}>
        {children}
      </button>
    );
  }
  return false
}

例子大概的意思就是:显示Sign Up按钮,隐藏Log in按钮,通过use读取上下文主题dark,设置按钮的背景色。

ref

ref 是一个常用的对象,通过useRef生成,在19版本中,也发生了变化。

React 19 开始,你现在可以将 ref 作为函数组件的 prop 来访问:

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

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

在过去,我们只能通过第二个参数来接收ref,现在像属性一样去接受再传递给其它组件。

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

说实话,看到这儿还是有点高兴的,过去的forwardRef+ref确实有点坑人,使用太过于麻烦。

还有一个特性,就是ref支持回调函数:

<input
  ref={(ref) => {
    // ref created

    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () => {
      // ref cleanup
    };
  }}
/>

当组件销毁时,会执行回调函数。

Context

上下文对象,在过去也会经常用,主要用于父子组件数据传递。

过去使用方式:

const ThemeContext = createContext('');

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

现在的方式:

const ThemeContext = createContext('');

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

useDeferredValue

useDeferredValue 是一个 React Hook,它允许你延迟更新 UI 的一部分。

useDeferredValue(value, initialValue?)

你可以把它理解为输入框的防抖,在过去我们输入框频繁输入,需要使用ahooks里面的useDebounceEffect来实现,现在我们可以直接用useDeferredValue来做了。

简单的例子:

import { Suspense, useState, useDeferredValue } from 'react';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

通过const deferredQuery = useDeferredValue(query);定义的值,会延迟更新,从而实现一定程度上的防抖功能。

支持文档元数据

HTML 中,文档元数据标记(如 <title><link><meta>)保留用于放置在文档的 <head> 部分中。在 React 中,决定哪些元数据适合应用程序的组件可能离你渲染 <head> 的地方很远,或者 React 根本不渲染 。过去,这些元素需要手动插入到 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 equals em-see-squared...
      </p>
    </article>
  );
}

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

支持样式表

样式表,包括外部链接的 (<link rel="stylesheet" href="...">) 和内联的 (<style>...</style>),由于样式优先规则,需要在 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" />  
    </div>
  )
}

支持异步脚本

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

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

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent>
    </body>
  </html>
}

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

支持预加载资源

React 19 包含许多用于加载和预加载浏览器资源的新 API,以尽可能轻松地构建不会因低效资源加载而受阻碍的出色体验。

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
  preinit('https://.../path/to/some/script.js', {as: 'script' }) // loads and executes this script eagerly
  preload('https://.../path/to/font.woff', { as: 'font' }) // preloads this font
  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // preloads this stylesheet
  prefetchDNS('https://...') // when you may not actually request anything from this host
  preconnect('https://...') // when you will request something but aren't sure what
}

这些 API 可用于通过将其他资源(如字体)的发现从样式表加载中移出来优化初始页面加载。他们还可以通过预取预期导航使用的资源列表,然后在单击甚至悬停时预先加载这些资源,从而更快地进行客户端更新。

最后

介绍一下我开源的前端低代码平台:Marsview,操作简单、功能强大,体验贼好,可以高效完成前端交互页面搭建。

开源地址:github.com/JackySoft/m…

开源不到4个月,当前累计Star已突破1.3K.

image.png

你很难相信我们基于Marsview搭建了一套Ant Design Pro模板,未来还会上线更多的项目模板。

image.png

image.png