React 19 – 通过示例解释新的钩子

562 阅读8分钟

各位读者大家好!Web 开发是一个不断发展的领域。整个生态系统由不同的库和技术组成。React 是 Web 开发中使用最广泛的库之一。

时不时会有小版本发布。然而,今年,React 团队发布了一个重要公告,推出了一个新版本 React 19。2024 年 4 月 25 日,React 正式向公众发布了 React 19 的测试版。

此版本包含大量新功能以及新钩子。在本文中,我们将讨论新版本中的四个新钩子:

  • useFormStatus

  • useActionState

  • useOptimistic

  • use

现有的表单处理实现

在学习第一个钩子之前,让我们看看我们目前如何在 React 中处理表单:

import { submitAction } from "./actions";

const FormHandling = () => {
  const [name, setName] = useState("");
  const [pending, setPending] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setPending(true);
    await submitAction({ name });
    setPending(false);
    setName("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        placeholder="Enter your name"
        onChange={(e) => setName(e.target.value)}
      />
      <button type="submit">Submit</button>
      {pending && <p>Submitting {name}...</p>}
    </form>
  );
};

通常,我们使用受控表单来设置表单数据,并通过表单的 onSubmit 属性将表单提交到服务器。此外,我们使用一个 pending 状态变量来处理提交待处理状态。 接下来我将展示的三个钩子将改变我们处理表单的方式。

useFormStatus

useFormStatus 钩子用于在提交表单时提供有关表单状态的信息。该钩子是 React DOM 的一部分,因此需要从 react-dom 导入:

import {useFormStatus} from 'react-dom'

这个钩子的使用方式如下:

  const { pending, data } = useFormStatus();

这个钩子不接受任何参数,并返回一个包含有关表单状态的信息的对象:

  • pending,这是一个布尔值,指示表单是否处于待处理状态。
    data 是一个 FormData 类型的对象,包含表单字段的值。

这个 钩子必须在包含

元素的父组件内使用。它只能返回这个父表单元素的状态信息,而不是返回当前组件中渲染的 元素的状态信息。
以下操作无效:

const {pending, data} = useFormStatus()
return (
    <form action={submit}></form>
  );

让我们将上面的表单写在一个单独的组件中:

import { submitAction } from "../../actions";
import { useFormStatus } from "react-dom";

const Form = () => {
  const { pending, data } = useFormStatus();

  return (
    <div>
      <input type="text" name="username" placeholder="Enter your name" />
      <button disabled={pending} type="submit">
        Submit
      </button>
      {pending && <p>Submitting {data?.get("username")}...</p>}
    </div>
  );
};

const FormStatusWithHooks = () => {
  return (
    <form
      action={async () => {
        await submitAction();
      }}
    >
      <Form />
    </form>
  );
};

在这个表单中,我们没有使用 onSubmit,而是使用了 form 元素的 action 属性。同时,我们使用了 data 对象来访问表单字段并渲染表单。

你可以在父 元素内使用任意数量的表单,并使用 useFormStatus 钩子获取表单状态。因此,你不需要每次都实现相同的逻辑。这提高了代码的可重用性。

useActionState

useActionState 可以通过以下方式使用:

const [state, formAction] = useActionState(submitData, initialState);
  • ActionState钩子接受以下参数: submitData,当表单提交时调用的函数。此函数应接受两个参数:当前状态和一个 FormData 对象。
  • initialState,表单未提交时状态的初始值。

useActionState 返回以下内容的数组: 

state,正在组件中渲染的当前状态。此状态等于初始状态。
formAction,您可以将其传递给表单元素的 action 属性的新动作。这将执行您使用当前状态传递的动作,并返回一个新的、更新的状态。
我们通过以下表单来了解如何使用这个钩子:

<form action={formAction}>
      <div>
        <input type="text" name="username" placeholder="Enter your name" />
        <input type="number" name="age" placeholder="Enter age" />
        <button type="submit">Submit</button>
      </div>
    </form>

这个表单提交用户信息,姓名和年龄,它会将用户信息附加到存储为状态的用户列表中。让我们使用 useActionState 钩子来获取状态:

import { useActionState, useEffect } from "react";
import { submitActionWithCurrentState } from "../../actions";
const ActionStateComponent = () => {
  const [state, formAction] = useActionState(submitActionWithCurrentState, {
    users: [],
    error: null,
  });

return <form action={formAction}>...</form>

submitActionWithCurrentState 方法返回一个修改后的用户列表组成的新状态,并且如果具有给定名称的用户已经存在,则会出现错误。让我们渲染用户列表和错误(如果存在的话):

<div className="error">{state?.error}</div>
      {state?.users?.map((user) => (
        <div key={user.username}>
          Name: {user.username} Age: {user.age}
        </div>
      ))}

在提交表单后,组件会重新渲染并更新组件的状态。

                                               已提交用户列表存储为状态
如果您尝试提交现有用户名,则会收到错误:

useOptimistic

顾名思义,useOptimistic 可以在异步操作仍在进行时优化地更新用户界面。当用户提交表单更改用户界面时,这个钩子可以在表单仍在提交时 “乐观地 ”显示预期结果。

  • 如果表单提交成功,用户界面将保持不变。
  • 如果表单提交失败,用户界面会恢复到之前的状态。

这样,即使表单提交需要时间,您也可以立即更新用户界面:

const [optimisticState, setOptimisticState] = useOptimistic(actualState, updateFn);

useOptimistic 钩子需要以下参数:

  • actualState,实际状态(actualState),即无操作时的乐观状态值。
  • updateFn(可选),它是一个函数,用于接收实际状态和传递给 setOptimisticState 方法的值,并计算 optimisticState。如果未指定此参数,则乐观状态等于新值。

返回值如下:

  • optimisticState,是在操作等待时显示的Optimisti值。
  • setOptimisticState,这是一个将Optimisti状态设置为新值的函数。

我们创建一个表单,执行异步操作来更改页面标题:

import { useOptimistic, useState } from "react";

const OptimisticComponent = () => {
  const [title, setTitle] = useState("Title");
  const [optimisticTitle, setOptimisticTitle] = useOptimistic(title);
  const [error, setError] = useState(null);
  const pending = title !== optimisticTitle;
  const handleSubmit = async (formData) => {
   
  };
  return (
    <div>
      <h2>{optimisticTitle}</h2>
      <p> {pending && "Updating..."} </p>
      <form action={handleSubmit}>
        <input type="text" name="title" placeholder="Change Title" />
        <button type="submit" disabled={pending}>
          Submit
        </button>
      </form>
      <div className="error">{error && error}</div>
    </div>
  );
};

在上面的代码中:

  • 我们正在显示标题的optimistic值,当没有任何操作待执行时,该值将被设置为标题状态。
  • 我们定义了一个待定变量,如果当前状态和optimistic状态不匹配,该变量将被设置为 true。根据该变量,我们将显示待执行文本并禁用按钮
  • 如果异步操作出错,我们会显示一个错误。

现在,我们调用我们的异步函数来解析或拒绝我们的请求:

import { submitTitle } from "../../actions";

const OptimisticComponent = () => {
  
  ...
  
  const handleSubmit = async (formData) => {
    setError(null);
    setOptimisticTitle(formData.get("title"));
    try {
      const updatedTitle = await submitTitle(formData);
      setTitle(updatedTitle);
    } catch (e) {
      setError(e);
    }
  };
  
  ...
  
};

在上面的代码中:

  • 我们已经按照最初的方法更新了状态。
  • 在调用异步操作之前,我们将optimistic状态设置为刚刚提交的更新标题。 
  • 如果承诺被拒绝,乐观状态就会恢复到原来的状态,因此无需再次设置。

use

use 方法并未作为钩子发布,而是作为 React API 的一部分。与 React 挂钩不同,use 也可以在iffor语句中调用。不过,use 只能在组件或钩子中调用。
通过 use,您可以读取组件中 Promise 或 Context 的值。我们来了解一下它在读取上下文和承诺时的用法。

读取上下

对于上下文,use 的作用与 useContext 钩子类似。它会返回上下文提供的值,供组件使用:

import { createContext, use } from "react";
import "../../styles.css";

const ThemeContext = createContext(null);
const UseHookWithContext = () => {
  return (
    <ThemeContext.Provider theme="dark">
      <MyComponent />
    </ThemeContext.Provider>
  );
};

const MyComponent = () => {
  const theme = use(ThemeContext);
  return (
    <div className={`myContainer theme-${theme}`}>
      <h2>Hi There!</h2>
    </div>
  );
};

上述代码的输出结果如下:

useContext 不同,use 提供了更大的灵活性,因为它也可以使用 if 语句有条件地使用:

const CodeSnippet = ({canShow}) => {
    if(canShow) {
        const theme = use(ThemeContext)
        return <h2>Code snippet shown with {theme} theme </h2>
    }
    return null
}

读取承诺的解析值
use 可返回承诺的解析值,供组件内部使用。它与暂停应用程序接口(Suspense API)集成,可显示一条临时消息,直到承诺得到解决。
让我们创建一个客户端组件,其中包含一个从服务器组件作为道具传递下来的承诺:

"use client";

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

const DataContainer = ({ dataPromise }) => {
  return (
      <Suspense fallback={<p>Fetching Data...</p>}>
        <DataComponent dataPromise={dataPromise} />
      </Suspense>
  );
};

const DataComponent = ({ dataPromise }) => {
  const data = use(dataPromise);
  return <div>{data && data}</div>;
};

在上面的代码中:

  • 在服务器组件中创建的承诺会作为道具传递给客户端组件。这反过来又可以传递给use方法。这样,客户端组件就能读取承诺的解析值。
  • Suspense API 会在承诺挂载时渲染传递给后备props的元素。

点击按钮时显示的数据
如果承诺被拒绝,有两种方法可以向用户显示错误:

  1. 在 promise 的 catch 块中返回错误信息,并将其视为已解决的 promise:

    export function fetchData() {
      return new Promise((resolve, reject) => {
        ...
        // ...
        
      }).catch((err) => err);
    }
    
  2. 使用 ErrorBoundary 包裹组件:

拒绝允诺时显示的错误边界

这还不是终点。React 19 还有很多新功能。您可以查看文档中的所有新功能。
目前,React 19 仍处于测试版,因此请勿将其用于生产系统。不过,您可以通过在 package.json 文件中添加以下依赖项并运行 npm i 来安装 React 19 canary 版本:

"dependencies": {
    "react": "canary",
    "react-dom": "canary",
  },

结论

React 团队引入了几个有用的钩子,它们将为开发人员带来更多便利。 前三个钩子:

useFormStatus、useActionState 和 useOptimistic 改变了我们处理表单的方式。使用钩子可以非常方便地获取组件内承诺的解析值。 

 在这篇文章中,我解释了每个钩子的语法,并演示了它们的使用示例。这一定会帮助你理解这些钩子以及如何使用它们。希望这对你今后的项目有所帮助。

 如果您有任何疑问或需要进一步说明,请告诉我。我们非常重视并感谢您的反馈!在 Twitter 上与我联系,了解更多更新和讨论。感谢您的阅读,期待下次再见!