React 中的异步表单验证很难 — 这里有一种可预测的解决方案

发布: (2025年12月31日 GMT+8 21:02)
5 min read
原文: Dev.to

Source: Dev.to

请提供需要翻译的具体文本内容,我将按照要求保留原始格式、代码块和链接,仅翻译正文部分。

核心问题:验证不具确定性

大多数表单解决方案基于事件进行验证,而不是基于状态快照。

示例异步验证器

async function validateEmail(value: string) {
  return api.checkEmailAvailability(value);
}

想象用户快速输入:

  1. 输入 taken@example.com → 异步请求 A 开始
  2. 更改为 john@example.com → 异步请求 B 开始
  3. 请求 A 在请求 B 之后完成 ❌

UI 显示 “邮箱已被占用”(不正确)。
较旧的异步结果覆盖了更新的输入——这是一种经典的竞争条件。

问题 #1:异步验证竞争条件

目标: 只应考虑最新的验证结果。

许多表单库:

  • 不跟踪异步验证的执行
  • 不将验证绑定到稳定的值快照
  • 允许过时的结果获胜

正确解决方案必须做到

  • 跟踪异步验证尝试
  • 忽略过时的异步结果
  • 始终基于一致的值快照进行验证

没有这些,异步验证将永远不可预测。

Problem #2: 跨字段验证脆弱

实际表单经常需要一起验证多个字段,例如:

  • 确认密码必须与密码匹配
  • 结束日期必须晚于开始日期
  • 只有在另一个字段启用时才需要某个字段

常见做法依赖于监听其他字段、手动重新验证或隐藏的依赖关系,这会引入难以调试的隐式行为。

显式跨字段验证

register("confirmPassword", {
  validate: (value, values) =>
    value !== values.password ? "Passwords do not match" : undefined,
});
  • 没有魔法
  • 没有自动重新验证
  • 没有隐藏的依赖——只有可读的逻辑。

Problem #3:提交行为与更改不同

“验证在更改时有效,但提交时表现不同。”
这发生是因为许多库:

  • 仅验证已触碰的字段
  • 在提交时跳过未触碰的依赖字段

结果:出现意外的提交时错误。

解决方法的简单规则

在提交时,验证所有已注册的字段。永远如此。这使得提交行为可预测且正确。

解决方案:可预测的、异步优先的表单引擎

这些问题促使我们创建了 Formora,一个围绕严格原则构建的无头 React 表单引擎:

  • 验证时机是显式的(change | blur | submit
  • 异步验证安全防止竞争条件
  • 验证始终基于值快照运行
  • 跨字段验证是显式的
  • 不会自动重新验证依赖

使用 Formora 的真实示例

异步邮箱验证 + 确认密码

const form = useForm({
  initialValues: {
    email: "",
    password: "",
    confirmPassword: "",
  },
  validateOn: "change",
  asyncDebounceMs: 500,
});

{
  await new Promise((r) => setTimeout(r, 300));
  if (value.includes("taken")) return "Email already taken";
},
})}
/>

value !== values.password ? "Passwords do not match" : undefined,
})}
/>

这为你提供了:

  • 永不显示过时错误的异步验证
  • 明确的跨字段逻辑
  • 可预测的提交行为
  • 干净、易于调试的代码

为什么其他开发者可以受益于 Formora

Formora 并不是要取代所有表单库;当你需要以下特性时,它表现出色:

  • 正确的异步行为
  • 明确的验证逻辑
  • 类型安全、可预测的状态
  • 在真实应用中可调试的表单行为

如果你的应用拥有异步验证、跨字段规则或复杂的提交逻辑,Formora 能提供稳固且可预测的基础。

最后思考

表单之所以变得困难,并不是因为它们本身复杂,而是因为验证的正确性常常被忽视。异步逻辑、字段之间的关系以及真实的用户行为都需要可预测性,而不是魔法。Formora 正是为了解决这些问题而明确且可靠地构建的。

有用的链接

  • GitHub:
  • npm:
Back to Blog

相关文章

阅读更多 »

SQL 让我感到不舒服。

在我实际的、非理论的理解中,object‑oriented programming 并不仅仅是传统 functional paradigm 的一种替代方案,而常常感觉像是……

Web生态系统内战

markdown !Javadhttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads...

React 编码挑战:卡片翻转游戏

React 卡片翻转游戏 – 代码 tsx import './styles.css'; import React, { useState, useEffect } from 'react'; const values = 1, 2, 3, 4, 5; type Card = { id: numb...