Use Cases (更新: 2026/6/2)

用 Claude Code 安全实现 React Hook Form 表单

从 useForm、Zod 校验、错误提示、提交状态到测试,讲解如何用 Claude Code 构建可靠表单。

用 Claude Code 安全实现 React Hook Form 表单

先定义表单契约,再让 Claude Code 动手

React Hook Form 是 React 中常用的表单库。它不要求你把每一次键盘输入都塞进组件 state,而是利用浏览器原生表单能力,再通过 registerhandleSubmitformState 管理字段、提交和错误。对初学者来说,这能把几个关键问题讲清楚:值从哪里来,什么时候校验,提交中如何防止重复点击。

Claude Code 可以一次生成组件、Zod schema、API route、测试和重构步骤。但表单通常连接着真实业务:咨询预约、产品试用、购买前调查、邮件订阅、客户资料更新。只说“帮我做一个表单”,很容易得到一个看起来不错、却缺少无障碍错误提示、服务端校验或提交中状态的实现。

本文用咨询表单做例子,说明如何使用 useFormzodResolver、字段错误、提交状态、服务端二次校验、测试,以及更安全的 Claude Code 提示词。React 组件整体思路可以参考Claude Code React 开发,Zod 规则设计可以继续看Claude Code Zod 校验

架构总览:把 schema 放在中心

React Hook Form 负责表单流程。Zod 负责描述什么输入是有效的。@hookform/resolvers/zod 提供的 zodResolver 则把两者接起来,让 React Hook Form 在校验时执行 Zod schema。

flowchart TD
  A["用户输入"] --> B["React Hook Form register"]
  B --> C["zodResolver 执行 schema 校验"]
  C --> D{"输入有效吗"}
  D -->|否| E["显示字段错误"]
  D -->|是| F["handleSubmit 提交数据"]
  F --> G["API 复用同一个 schema 校验"]
  G --> H["保存、通知或同步到 CRM"]

简单说,useForm 是表单控制器,Zod schema 是规则表,resolver 是连接器。让 Claude Code 修改表单时,明确这三部分能减少误改。之后要新增字段或选项,也可以要求它同时更新 schema、组件、API 和测试,而不是只改界面。

核对细节时建议看官方资料:React Hook Form 的useForm 文档React Hook Form ResolversZod API 文档、React 的<input> 参考,以及 Claude Code 的overviewcommands

可复制的 Zod schema

先把校验规则放到独立文件里。下面的例子包含姓名、邮箱、分类、正文和联系同意。z.infer 会从 schema 推导 TypeScript 类型,避免“类型定义一份、运行时校验又一份”的双重维护。

// src/features/inquiry/inquirySchema.ts
import { z } from "zod";

export const inquirySchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "请输入姓名")
    .max(80, "姓名请控制在80个字符以内"),
  email: z
    .string()
    .trim()
    .email("请输入有效的邮箱地址"),
  category: z.enum(["consulting", "support", "billing"], {
    error: "请选择咨询分类",
  }),
  message: z
    .string()
    .trim()
    .min(10, "正文至少输入10个字符")
    .max(1000, "正文请控制在1000个字符以内"),
  agreeToContact: z.boolean().refine((value) => value, {
    message: "必须同意我们为回复而联系你",
  }),
});

export type InquiryFormValues = z.infer<typeof inquirySchema>;

category 使用 z.enum,是为了限制提交值只能是固定集合。实际项目中,这些值可能决定线索分配、邮件模板、客服队列或 CRM 字段。提示 Claude Code 时,最好同时写清“显示文案”和“提交值”,例如“显示为技术支持,提交值是 support”。界面可以本地化,数据值要稳定。

用 useForm 处理输入、错误和提交状态

下面是表单组件。mode: "onBlur" 表示用户离开输入框时进行校验。对面向普通用户的咨询表单来说,这比每输入一个字符就显示红字更友好。真正提交时,handleSubmit 仍会做最终校验。

// src/features/inquiry/InquiryForm.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { inquirySchema, type InquiryFormValues } from "./inquirySchema";

async function sendInquiry(values: InquiryFormValues) {
  const response = await fetch("/api/inquiry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(values),
  });

  if (!response.ok) {
    throw new Error("Failed to send inquiry");
  }
}

export function InquiryForm() {
  const {
    register,
    handleSubmit,
    reset,
    setError,
    formState: { errors, isSubmitting },
  } = useForm<InquiryFormValues>({
    resolver: zodResolver(inquirySchema),
    mode: "onBlur",
    defaultValues: {
      name: "",
      email: "",
      message: "",
      agreeToContact: false,
    },
  });

  const onSubmit = async (values: InquiryFormValues) => {
    try {
      await sendInquiry(values);
      reset();
    } catch {
      setError("root", {
        type: "server",
        message: "发送失败,请稍后再试。",
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="name">姓名</label>
        <input
          id="name"
          autoComplete="name"
          aria-invalid={errors.name ? "true" : "false"}
          aria-describedby={errors.name ? "name-error" : undefined}
          {...register("name")}
        />
        {errors.name && (
          <p id="name-error" role="alert">
            {errors.name.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="email">邮箱</label>
        <input
          id="email"
          type="email"
          autoComplete="email"
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={errors.email ? "email-error" : undefined}
          {...register("email")}
        />
        {errors.email && (
          <p id="email-error" role="alert">
            {errors.email.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="category">咨询内容</label>
        <select
          id="category"
          aria-invalid={errors.category ? "true" : "false"}
          aria-describedby={errors.category ? "category-error" : undefined}
          {...register("category")}
        >
          <option value="">请选择</option>
          <option value="consulting">导入咨询</option>
          <option value="support">技术支持</option>
          <option value="billing">账单或合同</option>
        </select>
        {errors.category && (
          <p id="category-error" role="alert">
            {errors.category.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="message">正文</label>
        <textarea
          id="message"
          rows={6}
          aria-invalid={errors.message ? "true" : "false"}
          aria-describedby={errors.message ? "message-error" : undefined}
          {...register("message")}
        />
        {errors.message && (
          <p id="message-error" role="alert">
            {errors.message.message}
          </p>
        )}
      </div>

      <label>
        <input type="checkbox" {...register("agreeToContact")} />
        我同意你们为回复本次咨询而联系我
      </label>
      {errors.agreeToContact && (
        <p role="alert">{errors.agreeToContact.message}</p>
      )}

      {errors.root && <p role="alert">{errors.root.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "发送中..." : "发送咨询"}
      </button>
    </form>
  );
}

这里的关键不是样式,而是错误提示的位置和可访问性。每个字段都有 aria-invalid,错误文本有 role="alert",输入框用 aria-describedby 指向错误。这样既方便屏幕阅读器,也方便测试定位。更多界面可访问性可以参考Claude Code 可访问性实现

服务端也要复用同一个 schema

前端校验改善体验,但不能作为安全边界。用户可以绕过浏览器表单直接调用 API。因此 API 端也必须复用 schema,先检查 payload,再执行保存、邮件通知或 CRM 同步。

// app/api/inquiry/route.ts
import { NextResponse } from "next/server";
import { inquirySchema } from "@/features/inquiry/inquirySchema";

export async function POST(request: Request) {
  const payload = await request.json().catch(() => null);
  const parsed = inquirySchema.safeParse(payload);

  if (!parsed.success) {
    return NextResponse.json(
      {
        error: "Invalid inquiry",
        fields: parsed.error.flatten().fieldErrors,
      },
      { status: 400 },
    );
  }

  // TODO: 在这里保存到数据库、发送邮件或同步 CRM。
  return NextResponse.json({ ok: true });
}

让 Claude Code 增加 API 时,可以明确写:“复用 inquirySchema,校验失败返回 400 和字段错误,正式邮件发送和 CRM 集成先保留 TODO。”这样第一版更容易审查,密钥、重试、重复提交处理可以作为独立任务。

测试表单行为

表单的错误很容易被肉眼忽略。至少要测试空提交、有效提交、服务端失败和提交按钮禁用。下面用 Vitest 与 React Testing Library 验证“空提交显示错误”和“合法输入会调用 fetch”。

// src/features/inquiry/InquiryForm.test.tsx
import { afterEach, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InquiryForm } from "./InquiryForm";

afterEach(() => {
  vi.unstubAllGlobals();
});

test("空提交时显示校验错误", async () => {
  render(<InquiryForm />);

  await userEvent.click(screen.getByRole("button", { name: "发送咨询" }));

  expect(await screen.findAllByRole("alert")).toHaveLength(5);
});

test("输入有效时提交到API", async () => {
  const fetchMock = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
  vi.stubGlobal("fetch", fetchMock);
  render(<InquiryForm />);

  await userEvent.type(screen.getByLabelText("姓名"), "Masa");
  await userEvent.type(screen.getByLabelText("邮箱"), "masa@example.com");
  await userEvent.selectOptions(screen.getByLabelText("咨询内容"), "consulting");
  await userEvent.type(
    screen.getByLabelText("正文"),
    "我想安全地导入 React Hook Form。",
  );
  await userEvent.click(
    screen.getByLabelText("我同意你们为回复本次咨询而联系我"),
  );
  await userEvent.click(screen.getByRole("button", { name: "发送咨询" }));

  expect(fetchMock).toHaveBeenCalledWith(
    "/api/inquiry",
    expect.objectContaining({ method: "POST" }),
  );
});

你可以要求 Claude Code 先写失败测试,再补实现。端到端流程可参考Claude Code Playwright 测试。如果要追踪业务效果,成功提交后再触发 analytics,而不是仅统计按钮点击;相关思路可看Claude Code 分析埋点

给 Claude Code 的安全提示词

好的表单提示词要包含范围、约束、测试命令和不要做的事。可以从下面这个模板开始:

请用 React Hook Form 和 Zod 实现咨询表单。

范围:
- 只修改 src/features/inquiry 和 app/api/inquiry
- 使用 useForm、zodResolver,并从 schema 推导 TypeScript 类型
- 字段包括 name, email, category, message, agreeToContact
- 字段错误使用 role="alert",并设置 aria-describedby
- isSubmitting 为 true 时禁用提交按钮
- API route 复用同一个 Zod schema 做 safeParse
- 添加 Vitest + Testing Library 测试

验证:
- npm test -- InquiryForm
- npm run typecheck

不要做:
- 不新增 UI 库
- 不重命名已有 category 值
- 不在本任务里实现正式邮件、CRM 或密钥处理

修改需求也要具体。不要只说“加一个分类”,而要说“新增显示文案 培训咨询,提交值 training,同时更新 schema enum、select、API 校验、测试和分析映射”。Claude Code 会搜索相关文件,但契约仍然应该由人来定义。

典型用途和设计差异

用途推荐结构注意点
咨询表单Zod + React Hook Form + API 二次校验统计成功线索,不要只统计按钮点击
个人资料编辑defaultValues 放入已有数据保存后 reset(savedValues) 清掉 dirty 状态
购买前问卷select、radio、checkbox 组合提交值要和商品或 CRM ID 对齐
管理后台搜索轻量校验并同步 URL query避免每个按键都请求 API

共同原则是:界面文案和提交值分开。文案可以翻译,可以改写;提交值必须稳定,因为报表、自动化和后端代码都依赖它。让 Claude Code 生成表单前,先给它一张“显示文案 / 提交值”的小表。

常见失败

第一,只在前端校验。前端校验是体验,不是安全。API route 也要 import 同一个 schema,并在处理数据前调用 safeParse

第二,isSubmitting 很快恢复。原因通常是 onSubmit 没有 await 异步请求。fetch、数据库、邮件发送都要返回或等待 Promise,失败时用 setError("root", ...) 展示。

第三,错误提示离字段太远。只在顶部显示“有错误”不够。每个字段下方要有明确错误,顶部摘要只能作为补充。

第四,让 Claude Code 随手引入新的 UI 体系。如果项目已有 TextFieldSelectButton、toast,就在提示词里要求复用。表单任务里新增 UI 库会放大审查范围。

第五,忘记提交后的路径。咨询表单需要成功提示、感谢页、邮件通知、analytics 事件,有时还要 CRM 同步。把这些作为明确任务,避免表单看起来完成但业务没有闭环。

收益导线中的表单

表单质量不只看外观。真正要看的是它支持什么漏斗:免费 PDF 注册、商品线索、付费模板购买、导入咨询。让 Claude Code 重构前,先定义业务事件,再决定减少字段、改错误文案还是增加测试。

如果你想系统化学习,可以查看教材与产品;如果要把 Claude Code 表单改造用于团队流程,可以从培训与咨询开始。表单只是小组件,却经常是内容和收入之间的关键入口。

实际测试结果

Masa 在一个小型咨询流程中试了这个结构。最有效的改动是把 schema 集中到一个文件,因为它避免了“UI 增加了选项,API 允许值却忘记更新”的错误。第二个有效点是空提交和有效提交两条测试。后续让 Claude Code 修改表单时,测试很快发现了错误提示遗漏和 fetch 调用损坏。实际维护中,把表单当成“输入契约”,比只把它当成 UI 组件更稳。

#Claude Code #React Hook Form #React #表单 #校验
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。