R44第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

表单与异步校验

表单与异步校验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能构建一个带异步唯一性检查的多字段表单:输入防抖后校验、只有最后一次校验结果生效、提交状态与字段错误清晰分离、字段依赖变化时主动触发重校验。

问题场景

注册表单的用户名要查重(异步,几百毫秒)。第一版的 bug 图鉴:快速输入 "tom" 再改成 "tommy",先发的 "tom" 校验结果后到,页面显示「tom 可用」——但输入框里已经是 tommy;校验还在进行,提交按钮却能点;改了密码,确认密码的错误提示还停留在旧规则上。这些症状共享一个根因:异步校验结果与新输入之间,没有「谁新谁旧」的约定。

核心解释

三件事把表单校验做稳。

**一、请求序号守卫:只有最新输入的校验结果能落地。**结果必须带上「它校验的是哪个值」,落地前先比对:

useEffect(() => {
  if (!username) { setUsernameCheck(null); return; }
  let alive = true;
  const timer = setTimeout(() => {
    checkUsername(username).then((ok) => {
      if (alive) setUsernameCheck({ value: username, ok }); // 带上它校验的值
    });
  }, 300);
  return () => { alive = false; clearTimeout(timer); };
}, [username]);

// 判定时:
const checkValid = check && check.value === username && check.ok;

value 不相等就是过期结果,不显示——这一道比对挡住所有乱序问题,与请求竞态的保护同一血统。

**二、提交状态独立于校验状态。**校验进行中(checking)与提交中(submitting)是两条线;提交按钮在「校验中 / 有错误 / 提交中」三种情况下禁用,并让用户知道在等什么。

**三、字段依赖主动重校验。**确认密码依赖密码、结束日期依赖开始日期:把依赖写进 useEffect 的依赖数组,被依赖字段一变,依赖字段的校验结果要么重算、要么立即标脏——让机制而不是记性来保证:

const canSubmit = checkValid && password && !confirmError && !submitting;

示例拆解

实验区注册表单三个字段:用户名(异步查重,400ms)、密码、确认密码:

  • 输入用户名,停手 300ms 后出结果;快速改写时,旧结果落地前被丢弃,页面显示「已丢弃过期校验」计数;
  • 输入被占用的名(如 admin),提示「已被注册」并阻止提交;
  • 改密码后,确认密码的「与密码不一致」提示立即刷新(确认密码是派生校验);
  • 提交:校验齐全才可点,提交中按钮显示「提交中…」并防重复;
  • 验证:快速输入 → 改写 → 立即提交,确认提交用的是最新值且无旧提示残留。

动手任务

打开本课实验区:

  1. 把防抖调成 1000ms,快速输入后立即点提交:体验「校验中禁止提交」,并在按钮旁写上等待原因。
  2. 去掉 check.value === username 的比对,复现「旧结果覆盖新输入」,记录复现步骤,再恢复。
  3. 新增「邮箱(可选):填了就查格式与唯一性」,复用同一套守卫模式。

常见错误

  • 校验结果不带输入值就落地:乱序必现「张冠李戴」;结果必须与触发它的输入绑定。
  • 提交与校验共用一个 loading:用户不知道在等查重还是在等提交;两条状态线分开。
  • 只在 blur 时校验:输入过程中依赖字段已经变了,提示还是旧的;依赖变化时标脏或重校。
  • 错误提示不随输入清除:改对了还挂着旧错误;输入变化时清除或重算本字段错误。

检查与复盘

自查清单:

  • 能解释「过期校验结果」如何被 value 比对挡住
  • 提交禁用的三种原因(校验中 / 有错误 / 提交中)都能被用户看见
  • 字段依赖(密码 ↔ 确认密码)的重校验由机制驱动,验证过

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(React 18+):注册表单在输入变化后立即请求校验用户名。用户先输入「ab」(请求 A,300ms 后返回),又输入「abc」(请求 B,100ms 后返回)。最终提示与输入框对应吗?

function UsernameField() {
  const [name, setName] = useState('');
  const [msg, setMsg] = useState('');

  useEffect(() => {
    if (!name) return;
    fetch('/api/check?name=' + name)
      .then((r) => r.json())
      .then((d) => setMsg(d.taken ? '已被占用' : '可用'));
  }, [name]);

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>{msg}</p>
    </>
  );
}
Q2故障定位中等

bug 现场:邮箱输入框失焦时异步校验格式。用户输入「a@b.co」校验通过(显示绿色「可用」),随后继续编辑成「a@b」(还没失焦)。此时提示区显示什么?为什么会误导用户?怎么修?

function EmailField() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState('idle'); // ok | bad | checking

  function onBlur() {
    validateEmail(email).then((ok) => {
      setStatus(ok ? 'ok' : 'bad');
    });
  }

  return (
    <>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={onBlur}
      />
      {status === 'ok' && <p style={{ color: 'green' }}>可用</p>}
      {status === 'bad' && <p style={{ color: 'red' }}>格式不正确</p>}
    </>
  );
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...