React 19的新表单特性还能用React Hook Form吗?
React 19 中的一个重大变化是它处理表单和表单提交的方法,这对其处理表单的方式产生了影响。这使得 React Hook Form 不再像以前那么必要,基本上推迟了这个必要性。
我们就从行动开始吧。
操作工作NextJS 长期以来一直在推广“服务器端动作”这一概念。这些是简单的异步函数,通常用于处理数据获取或发送。它们设计为在服务器上运行,而不是在客户端,而且有相应的指令来实现这一目标。
React 19 延续了这一理念,但不仅限于“服务器”动作,而是更泛化的动作,具有特定的签名格式。
const 保存操作 = async (previousState, formData) => {
// 保存操作的实现,用于保存表单数据和之前的状态。
}
这个 formData
参数特别有趣。这不仅仅是一些任意的数据,而是一个实际的 HTML 的 FormData 对象。这意味着它有一些特定的方法,比如 formData.get("email")
。它不像普通的 pojo 对象那样,你不能像这样直接访问 formData.email
。
这也意味着如果你得到了该值,你必须进行转换,才能将其作为JSON负载发送。默认情况下,它将以multipart/form-data
发送,而不是JSON。这只需要一行代码就能搞定。
const payload = Object.fromEntries(formData.entries()) // 将表单数据转换为对象
不过这很容易被忽略,你可能没注意到需要这样做。
那个动作实际上做些什么就看你了。发送某种HTTP请求还挺常见的,我们就这么假设好了。
const 注册用户 = async (previousState, 表单数据对象) => {
const fde = 表单数据对象.entries();
const 数据负载 = Object.fromEntries(fde);
const { data: 数据 } = await axios.post(数据负载);
return 数据;
}
为了记录,我只是使用axios,因为这样会更短。显然,使用fetch也能达到同样的效果。
这个动作处理函数与你在 onSubmit 事件中使用的 handleSubmit
函数差不多。唯一的区别是这个函数不需要接收事件参数。没有什么多余的说明了,我们来看一下具体的例子。
现在你可以把动作功能用作表单的动作属性了。
<form action={handleSubmit} >
这可能对只熟悉 JavaScript 表单的人来说看起来很奇怪,但说实话,form action 就是我们以前用来提交表单的手段。它是我们要提交表单的 URL 地址。我们甚至还有表单提交的方法。这还真挺疯狂的。
总之,你会注意到这并没有一个事件。它既不是 onSubmit 也不是绑定到按钮的 onClick,这更像是一个“操作”,一种截然不同的处理方法。
这里有一个不错的点是,如果你有一个这样的操作,React 会为你处理表单的两个方便之处。一个是你不需要使用 event.preventDefault()
,另一个是你在提交后不需要重置表单,React 也会帮你搞定这两个问题。
这些动作本身可以直接使用,但如果你想获取更多功能,可以将它们包装在这些新的钩子中。其中最相关的是 useActionState
。它以前被称为 useFormState
,但后来决定该功能不仅限于表单,因此改用了更通用的术语 useActionState
。
useActionState
函数直接封装动作,返回一个值的元组。
const [response, submitAction, pending] = useActionState(RegisterUserAction) // 使用useActionState获取注册用户操作的状态
这特别有用的地方在于你可以从中看到有一个“响应属性(response property)”。有些人将其命名为“错误”,因为它可能返回错误信息。不过我不太理解为什么叫这个名字,因为它包含了从操作中返回的任何结果,所以没有必要特意称它为错误。
更详细地解释这一点,当我们回顾之前的例子时,会发现有点别扭。
const 注册用户 = async (previousState, formData) => {
const 用户数据 = formData.entries();
const 提交的数据 = Object.fromEntries(用户数据);
const { data } = await axios.post(提交的数据);
return data;
}
export default function RegisterForm() {
return (
<form action={注册用户}>
// 注释
</form>
);
}
我们有一个表单和一个动作,它们是绑定在一起的。我们的动作会创建并返回一个新的用户,带有ID等信息。但我们没有办法获取到该对象。整个结果都被吃掉了。虽然动作被用到了,但是它从未被返回。
useActionState
函数让我们可以获取这些东西的状态。
const 注册用户 = async (previousState, formData) => {
// 同上
return data;
}
export default function 注册表单() {
const [用户, 提交操作] = useActionState(注册用户);
if (用户 && !(用户 instanceof Error)) {
window.location.href = "/dashboard";
}
return (
<form action={注册用户}>
//
</form>
);
}
这并不好,也没有错误处理等,不过你知道大概的意思。元组的第一个元素是我们操作的响应,默认是未定义的。不过,useActionState
实际上还可以接受一个默认值作为第二个参数。当你想用空数组填充它,或者返回的结构中有不想留空的多个属性时,这会很有用。
在不断增长的 React 钩子列表里,另一个条目是 useOptimistic
。这个钩子适用于你希望在表单中进行乐观更新的情况。简单来说,当你希望假设请求成功并在此基础上更新 UI,并在假设请求成功的基础上更新 UI,然后在实际情况出现错误时回滚,这就是乐观更新。这篇文章不会详细探讨这些变化,我只是说一下,我不太喜欢这种模式,并且它需要对表单操作本身的结构进行一些调整才能让它正常工作。它不像 useActionState
,后者只需包装一个现有的动作即可。
最后一个要讨论的是 useFormStatus
。你可以把它想象成在 React Hook Form 中的 useFormContext
,只不过它只返回 isSubmitting
而已。这个钩子的目的是让你在嵌套组件内部可以访问表单状态信息的细节,例如。
React 19 的表单处理中最棒的一点可能是它管理输入字段的方式。如果你还不太熟悉的话,在 React 中,如果你只是直接在 JSX 中插入一个输入标签,最终会发现 React 会做这个特别有趣的事情:它看到输入标签后会重新渲染,这会导致输入内容被清空。
这意味着你需要遵循一种非常标准的模式——我在之前的文章中也这样做过——即把元素变成一个“受控输入”。
export default function MyInput() {
const [email, setEmail] = useState('');
return (
<input name="email" type="email" value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
)
这只是针对一个字段来说,所以你可以想象一下,如果有多个字段,追踪多个状态信息的话会有多复杂。这就是我之前文章的起点。
相比之下,React 19 认为输入值被视为“稳定”的,在React 19的渲染周期中不会重新渲染这些值。这样就使得标记更加合乎逻辑。
export default function MyInput() {
return <input name="email" type="email" />;
}
这里没状态,所以就不归 React 管。
如果你在想提交是怎么回事,因为你没有状态要发送,这确实是一个合理的问题。事实上,仍然存在“状态”。浏览器仍然知道状态,仍然在跟踪表单字段。它使用标准的网页 API 来更新字段,并用标准的网页 API 获取表单数据进行提交。
这真的是React退一步,让web回归原来的工作方式,我完全支持这种做法。满分,太棒了。
你要用得着 React Hook Form 吗?你可能不行,但简单来说就是这样。不过有一个关键点我们还没提到:验证。这就像往机器里扔了一颗螺丝刀,打乱了所有的计划。
React 19 并不处理表单验证。例如,它只支持标准的 HTML 表单必填字段和正则表达式,除此之外,它没有跟踪字段错误或阻止无效提交的功能。
React 19 的表单可能已经足够满足你的需求了,特别是对于简单的表单,比如搜索功能。但大多数实际应用中的表单往往需要一些验证,到这一步,你可能希望有一些工具或方法来处理这些问题。
这里有几个选项。一种是继续使用类似RHF的东西,我觉得这没问题。至少到目前为止,我选择了这个。另一种是采用一种更混合的方法,使用像Zod这样的工具生成一个模式,然后再手动核对这个模式。
值得注意的是,由于我们使用了action属性,onSubmit事件变得完全开放,这种做法是完全有效的。
const handleSubmit = (e) => {
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
// 安全解析用户注册数据
const result = registerUserSchema.safeParse(data);
if (!result.success) {
e.preventDefault();
// 把错误处理一下
}
}
<form action={RegisterUser} onSubmit={handleSubmit}>
这大概像这样——但错误处理要好很多——对于某些情形应该行得通。
不过我暂时还是会继续使用 React Hook Form。这就引出了一个问题。
不管怎样,这一切终将过去现在有一个问题,那就是既有 React 19,也有 NextJS。大多数情况下,NextJS 15 和 React 19 应该完全兼容,并且它们设计上是协同工作的。然而,这里有些不一致的地方。React 和 NextJS 都提供了“actions”,NextJS 使用的是服务器端 Actions,而 React 只提供 Actions(没有服务器端概念)。
这些意图相似,但实现方式略有不同。React Actions 需要一个名为 previousState
的第一个参数,然后是表单中的数据,而 Next Server actions 只需要表单中的数据。
这里有一个明显的设计缺陷,问题在于 React 19。首先,React 行动只能与表单一起使用。它们明确要求 formData
类型为 FormData
。它们不仅仅是“行动”。它们是表单行动。在使用和执行上没有灵活性。
特别奇怪的是,少数围绕这些操作构建的钩子之一——useActionState
——最初被命名为useFormState
,但他们将其更改为更通用的名字,但该钩子仍然特定于表单。
第二个明显的缺点是 React Actions 和 Next Server Actions 使用的参数的不一致。如果 React 的参数顺序是 formData
先、previousState
后,这样的顺序会更合理,这样 previousState
就会成为一个可选参数。
这不仅符合现有服务器和表单操作的主流实现,而且还符合基本的软件原则:参数按重要性顺序排列,表单数据总是比无关的用户界面状态更为关键。
React Hook Form 在 React 世界中的应用这只会越来越复杂。最好的情况是我们可以硬撑过去,直到其中一些API可以协同工作。不仅适用于表单,也适用于服务器组件,这是React 19新增的一个特性。(此处不赘述)
直到有与 R19 更好集成的 RHF 版本。或者直到有另一个处理表单生命周期的库。直到出现更优的处理 React 19 内置验证的方法或模式,我们可能还得继续使用在 React 19 中使用 React Hook Form 的次优处理方式。
对此我们需要保持清醒的头脑。没什么变化。我们会继续像现在这样使用RHF。我们只是无法享受到React 19简化内部状态的好处,还需要继续控制输入。虽然这有点遗憾,但这绝对不是世界末日。我们暂时还无法采用更好的方法,这种情况在 web 开发中也并不罕见。
对我来说,我建立了一些小型表单,完全放弃了React Hook Form——这在过去我从未尝试过。但对于较大和更复杂的表单,如多步骤表单或需要大量验证的表单,我仍然会选择使用React Hook Form。
共同学习,写下你的评论
暂无评论
作者其他优质文章