说一下React 19 对表单处理的改进:useActionState 和 useFormStatus?
React 19 表单处理的变革:useActionState 与 useFormStatus 详解
在 Web 开发中,表单处理是构建用户交互的核心环节。长久以来,React 开发者们习惯于使用 useState 来管理表单的各种状态,例如输入数据、提交中的 pending 状态、以及错误信息。这种方式虽然直观,但在复杂场景下往往会导致组件内部状态逻辑变得冗长和分散。
React 19 的到来,为我们带来了两个强大的新 Hooks:useActionState 和 useFormStatus。它们与 React Actions 协同工作,旨在简化表单状态管理,提供更优雅、更集中的逻辑处理方式,并优化用户体验。
回顾:传统表单的状态管理
在深入了解新特性之前,我们先回顾一下在 React 19 之前是如何处理表单异步提交的。通常,我们需要手动维护至少三个状态:
Pending 状态:用于在数据提交过程中禁用按钮、显示加载指示器。Error 状态:用于捕获并向用户展示提交过程中发生的错误。Data 状态:用于存储和更新表单数据。
让我们看一个典型的例子,一个更新用户名的表单:
// 在 React 19 之前的做法
function UpdateNameComponent() {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
const submissionError = await updateName(name); // 假设 updateName 是一个异步请求
setIsPending(false);
if (submissionError) {
setError(submissionError);
return;
}
redirect("/success-path"); // 提交成功后重定向
};
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}这段代码清晰地暴露了问题:isPending 和 error 的状态管理与核心的提交逻辑 handleSubmit 纠缠在一起,增加了代码的复杂性。随着表单逻辑的增加,这些状态管理代码会变得越来越难以维护。
useActionState:优雅地管理 Action 状态
React 19 引入了 Actions 的概念,允许我们将一个函数直接传递给 <form> 的 action 属性。useActionState Hook 则是专门为管理这些 Action 的状态而设计的。它接收一个 Action 函数和初始状态,返回一个包含当前状态、可供表单调用的新 Action,以及一个 pending 状态的数组。
它的签名如下:
const [state, formAction, isPending] = useActionState(actionFn, initialState);state:Action 执行后返回的状态。初始值为initialState。formAction:一个新的 Action,需要将其传递给<form>的action属性。React 会在表单提交时调用它。isPending:一个布尔值,表示 Action 是否正在执行中。
现在,我们用 useActionState 来重构上面的例子:
// 使用 useActionState
import { useActionState } from 'react';
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const newName = formData.get("name");
const error = await updateName(newName); // 同样的异步请求
if (error) {
// 直接返回错误,它将成为新的 state
return error;
}
redirect("/path");
return null;
},
null, // 初始 error 状态为 null
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}通过对比可以发现,useActionState 极大地简化了代码:
状态集中管理:将 pending 和 error 状态的处理逻辑收敛到了
useActionState内部。代码更简洁:我们不再需要手动调用
setIsPending和setError。Action 函数的返回值就是新的状态。语义更明确:代码的意图从“手动处理点击事件”转变为“声明一个表单 Action”,更加符合 React 的声明式思想。
useFormStatus:感知父表单的提交状态
在实际应用中,提交按钮或状态提示通常是独立的子组件。useActionState 的 isPending 状态在当前组件作用域内可用,但如果想在子组件中响应表单的提交状态,就需要通过 props 逐层传递,这非常繁琐。
useFormStatus Hook 优雅地解决了这个问题。它允许一个组件访问其所在的父级 <form> 的状态信息,而无需任何 props 传递。
重要前提:使用 useFormStatus 的组件必须被渲染在 <form> 标签内部。
useFormStatus 返回一个包含表单状态的对象,其中最常用的就是 pending 属性。
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
// 在表单中使用
function SomeForm() {
return (
<form action={someAction}>
<input name="field" />
<SubmitButton />
</form>
);
}在这个例子中,SubmitButton 组件不需要从 SomeForm 接收任何 props。它通过 useFormStatus 直接“感知”到外部 <form> 是否处于提交中,并相应地更新自己的 UI。这使得创建可复用的、与表单状态解耦的设计系统组件变得异常简单。
综合实践:构建一个购物车表单
现在,我们将 useActionState 和 useFormStatus 结合起来,构建一个功能完整的“添加到购物车”的表单。这个例子将展示如何处理成功和失败两种情况,并提供即时反馈。
actions.js (Server Action)
我们首先定义一个在服务器上运行的 Action。
'use server';
export async function addToCart(prevState, queryData) {
const itemID = queryData.get('itemID');
// 模拟 API 请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
if (itemID === "1") {
// 模拟成功
return {
success: true,
cartSize: 12, // 假设返回了新的购物车数量
};
} else {
// 模拟失败
return {
success: false,
message: "该商品已售罄。",
};
}
}AddToCartForm.js (Client Component)
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { addToCart } from "./actions.js";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "添加中..." : "添加到购物车"}
</button>
);
}
function AddToCartForm({ itemID, itemTitle }) {
// useActionState 管理 Action 的返回状态
const [formState, formAction] = useActionState(addToCart, {});
return (
<form action={formAction}>
<h2>{itemTitle}</h2>
<input type="hidden" name="itemID" value={itemID} />
<SubmitButton />
{/* 根据 Action 返回的状态显示不同的提示信息 */}
{formState?.success && (
<div className="toast">
添加成功!购物车现在有 {formState.cartSize} 件商品。
</div>
)}
{formState?.success === false && (
<div className="error">
添加失败: {formState.message}
</div>
)}
</form>
);
}
export default function App() {
return (
<>
<AddToCartForm itemID="1" itemTitle="JavaScript 权威指南" />
<AddToCartForm itemID="2" itemTitle="JavaScript 优良部分" />
</>
);
}在这个综合示例中:
AddToCartForm使用useActionState来调用addToCartAction 并接收其返回的状态formState。formState对象包含了success、cartSize或message等信息,组件可以直接用它来渲染不同的 UI 反馈。SubmitButton作为子组件,使用useFormStatus来独立管理自己的disabled和文本状态,完全不知道外部表单的具体逻辑。整个流程实现了状态管理与 UI 组件的完美分离,代码结构清晰,可维护性高。
结论
useActionState 和 useFormStatus 是 React 19 在表单处理方面迈出的重要一步。它们不仅显著减少了管理异步表单状态所需的模板代码,还通过关注点分离(Separation of Concerns)的原则,使得构建可复用、可预测的表单组件变得更加容易。
当我们拥抱这些新特性时,我们实际上是在拥抱一种更声明式、更强大的范式来处理用户交互。下一次你在 React 中构建表单时,不妨尝试一下这两个新的 Hooks,体验它们为开发流程带来的顺滑与高效。
