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

表单校验与提交

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

学习目标

玩具表单和真实表单的分水岭在提交之后:字段级校验、重复点击防护、服务端失败、成功反馈,一个都不能少。这一课把异步提交的完整生命周期补齐,读完你能写出一个「怎么折腾都不坏」的资料表单。

问题场景

个人资料表单第一版只有「校验通过就提交」两步:按钮没禁用,用户连点三次发出三个请求;服务端偶尔 500,页面直接白屏抛错;成功后没有反馈,用户不确定存没存上。这些都不是校验逻辑的问题,而是提交生命周期没有被完整建模。

核心解释

先把提交过程的状态数清楚:idle(可提交)→ submitting(请求中)→ saved(成功)或 error(失败,回到可改状态)。和 R05 一样,用互斥字段建模,天然排除「提交中和失败中并存」这类非法组合:

const [status, setStatus] = useState("idle");
const [fieldErrors, setFieldErrors] = useState({});

function validate() {
  const errs = {};
  if (!form.nickname.trim()) errs.nickname = "昵称必填";
  if (!/^\S+@\S+\.\S+$/.test(form.email)) errs.email = "邮箱格式不对";
  return errs;
}

async function handleSubmit(e) {
  e.preventDefault();
  const errs = validate();
  setFieldErrors(errs);
  if (Object.keys(errs).length > 0) return; // 校验不过不发请求
  setStatus("submitting");
  try {
    await saveProfile(form);
    setStatus("saved");
  } catch {
    setStatus("error");
  }
}

四个细节撑起可靠性:

一、校验在客户端、拦截在提交入口。 字段错误存成 { 字段名: 消息 } 对象,渲染时按字段显示;校验不过直接 return,请求根本不发出。

二、重复点击防护。 submitting 期间按钮禁用并改文案——比在处理器里加 if 判断更直观,状态本身就是闸门:

<button type="submit" disabled={status === "submitting"}>
  {status === "submitting" ? "保存中…" : "保存"}
</button>
{status === "saved" && <span>✅ 已保存</span>}
{status === "error" && <span>保存失败,请重试</span>}

三、服务端失败要可读、要保数据。 catch 里不能只 console.log:用户输入全部保留在受控 state 里(别清空表单),界面显示「保存失败,请重试」;如果是服务端返回的字段级错误(比如邮箱被占用),合并进 fieldErrors 显示到对应字段旁边。

四、成功要有反馈并落回稳态。 显示成功提示,几秒后或下次编辑时状态回到 idle,让表单可以继续被使用。

示例拆解

实验区是完整的资料表单,关键点如下:

  • 字段:昵称(必填)、邮箱(格式校验)、简介(选填);错误信息显示在对应输入框下方,聚焦该字段时立刻可见;
  • 三个模拟开关:「服务端失败」(500ms 后 reject)、「模拟慢网络」(2 秒延迟),配合提交按钮的禁用与文案变化,观察重复点击防护;
  • 提交日志面板记录每次状态迁移:idle → submitting → error / saved,与界面显示一一对应;
  • 服务端失败后输入内容原样保留,修正邮箱字段错误信息(「该邮箱已被注册」)来自服务端响应的模拟。

验证方式:开着「服务端失败」连点提交,请求不会叠加;关掉后一次成功,状态提示出现后自动回落。

动手任务

打开本课实验区,完成三个小改动:

  1. 简介字段加上限 50 字的校验,超限显示剩余字数提示。
  2. 成功后 3 秒自动回到 idle(用 setTimeout + 清理),并支持「再次编辑」。
  3. 把「邮箱被注册」这类服务端字段错误合并进 fieldErrors,定位显示到邮箱下方。

常见错误

  • 提交后清空表单再处理结果:失败时用户输入全丢;数据只在确认成功后才清理(本例的资料表单甚至不需要清)。
  • 用布尔值防重复提交:if (isSubmitting) return 与按钮禁用双轨并行容易漏;让互斥的 status 字段做唯一闸门。
  • catch 里吞掉错误细节:一律显示「出错了」让用户无从下手;区分网络错误与字段错误,字段错误要落到具体输入框旁。
  • 校验放在 onChange 里每次输入都全量跑:用户第一个字还没打完就报错;字段级即时校验通常从字段失焦(onBlur)开始,提交时做全量校验。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能画出提交状态机:idle / submitting / saved / error 及其迁移条件
  • 重复点击、失败保数据、成功反馈三个可靠性细节都能说清实现位置
  • 测试表单时会把失败、慢网络、连点三种情况都过一遍

参考资料

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

阅读代码(React 18+)。输入框为空时直接点击「提交」,页面上会出现什么?

function Signup() {
  const [name, setName] = useState('');
  const [error, setError] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (!name.trim()) {
      setError('昵称不能为空');
      return;
    }
    setError('');
    alert(`提交:${name}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type='submit'>提交</button>
    </form>
  );
}

读完这一课了?

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

Command Palette

Search for a command to run...