Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/项目笔记/代达罗斯/需求/COD-100

Form

7 分钟阅读 · Note

目录树 578 篇

              • Barrel Export
              • Button 与 Button Group
              • Form
            • 国际化
            • 需求整体流程
            • Linear
          • 项目待做
          • 性能优化
          • UI设计
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗Barrel Export同一路径↗Button 与 Button Group同一路径↗「Feature」Introduce Crate as an independent metadata entity[ˈentəti] | 引入 Crate 作为独立元数据实体共同主题↗国际化共同主题↗实施计划:Archetype 管理 (COD-86)共同主题↗需求整体流程共同主题
  • Form

Form


场景分析

项目中的Form

在 Daedalus 项目中,form是一套完整的前端表单架构规范:

┌─────────────────────────────────────────────────────┐
│                  Form Architecture                  │
│                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │ react-hook-  │  │ shadcn/ui    │  │ Zod (v4)  │  │
│  │ form         │  │ Form 组件层   │ │ Schema     │  │
│  │ 状态管理      │  │ UI 结构      │  │ 校验层     │  │
│  └──────┬───────┘  └──────┬───────┘  └─────┬─────┘  │
│         │                 │                │        │
│         └────────┬────────┴────────┬───────┘        │
│                  │                 │                │
│                  ▼                 ▼                │
│           useForm()          zodResolver            │
│                  │                 │                │
│                  └────────┬────────┘                │
│                           │                         │
│                           ▼                         │
│                    Form Component                   │
│                           │                         │
│                    仅提交时输出                      │
│                    数据副本给外部                    │
│                           │                         │
│                           ▼                         │
│              ┌──────────────────────┐               │
│              │Global State (Zustand)│               │
│              │ 或 API 调用           │               │
│              └──────────────────────┘               │
└─────────────────────────────────────────────────────┘

能力模型

层级职责包含要素
状态管理表单字段值的生命周期useForm、formControl、FormProvider、独立状态树、沙盒隔离
UI 结构标准化的组件层级Form → FormField → FormItem → FormLabel → FormControl → Input → FormMessage
校验集成类型安全的表单校验Zod schema、zodResolver、zod/v4 导入、schema 派生(.pick/.omit)
数据流初始化 → 编辑 → 提交的数据通道defaultValues 深拷贝、null→undefined 处理、提交时副本传递
复杂表单拆分多子组件的表单上下文共享FormProvider + useFormContext、禁止 props 透传 control

"两棵树"状态隔离模型

这是 Form 场景最核心的架构概念:

┌─────────────────────────────────────┐
│  Global State Tree (Zustand)        │
│  - 跨组件共享的业务状态               │
│  - 用户信息、路由状态、UI 开关        │
│  - 不包含任何表单字段值               │
└─────────────────────────────────────┘
         ↑ 仅在提交时传入数据副本
         │ (单向,不可反向流入)
┌─────────────────────────────────────┐
│  Form State Tree (react-hook-form)  │
│  - 字段值、脏值、错误、提交状态        │
│  - 完全独立,自包含生命周期           │
│  - defaultValues 初始化 → 用户编辑   │
│    → handleSubmit 输出副本           │
└─────────────────────────────────────┘

单向数据流:

  1. 初始化:defaultValues 接收原始数据的深拷贝副本(structuredClone 或 {...data})
  2. 编辑过程:所有用户输入仅在 react-hook-form 内部流转,原始数据不可变
  3. 提交时刻:handleSubmit((values) => onSubmit({...values})) 传递数据副本
  4. 禁止反向污染:Zustand 状态不应直接驱动表单字段值(除非是初始值)

两种合法范式

飞书文档 + 现有 Skill 定义了两种表单范式,必须先选定范式再按对应 Condition 约束:

范式适用场景formControl 位置Props
范式 A:独立表单组件跨页面复用、纯输入收集器组件内部 useForm()initialData + onSubmit
范式 B:页面绑定表单Dialog 内表单、单页面紧耦合Zustand slice 中 createFormControl<T>()无(从 slice 读取)

代码示例:

// 范式 A 示例:
// 组件接收 initialData + onSubmit,内部 useForm()
export const CreateCrateForm = ({
  initialData,
  onSubmit,
}: CreateCrateFormProps) => {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: initialData ? { ...initialData } : defaultValues,
  });
  // ...
};

// 范式 B 示例:
// formControl 在 slice 中,组件不接 props
// slice.ts:
const formControl = createFormControl<AgentFormValues>({
  defaultValues: { name: "", description: "" },
  resolver: zodResolver(agentFormSchema),
});
// component.tsx:
const formControl = useStore(store, (s) => s.agentFormControl);
const handleFormSubmit = useStore(store, (s) => s.handleFormSubmit);
const form = useForm<AgentFormValues>({ formControl: formControl.formControl });

Crate 设计

对齐 Button 和 Barrel Export 场景的层级拆分策略:

层级说明
功能模块级一个 Crate = 一组内聚的功能,不是单个组件
与路由/页面解耦Crate 不绑定到特定页面,可跨路由复用
类型语义明确使用 CrateTypeValues 中的枚举值
按"层"拆分引擎层 / UI 层 / 校验层 / 桥接层

Crate A: form-engine

字段值
nameform-engine
typelibrary
responsibility封装 react-hook-form 核心集成:useForm 工厂函数、FormProvider 上下文设置、formControl 生命周期管理、与 zodResolver 的基础绑定、沙盒隔离机制
metadata{"package": "react-hook-form", "version": "^7.x", "paradigm": ["A", "B"]}

包含内容:

  • useForm 的类型安全封装(泛型 FormValues、zodResolver 集成)
  • FormProvider + useFormContext 的标准用法
  • createFormControl&lt;T&gt;() 工厂(供 Zustand slice 使用)
  • 表单提交流程的标准化(handleSubmit → deep copy → callback)
  • 范式 A/B 的路由逻辑(initialData + onSubmit vs formControl + handleFormSubmit)

Crate B: form-components

字段值
nameform-components
typemodule
responsibility提供 shadcn/ui Form 系列组件的标准化封装:Form、FormField、FormItem、FormLabel、FormControl、FormMessage,确保字段层级结构一致
metadata{"package": "shadcn/ui", "components": ["Form", "FormField", "FormItem", "FormLabel", "FormControl", "FormMessage"]}

包含内容:

  • Form — useForm() 返回值展开,包裹表单
  • FormField — control + name → render={({ field }) =&gt; ...}
  • FormItem — 字段容器 + 错误状态上下文
  • FormLabel — 可选标签
  • FormControl — 包裹实际输入组件,传递 field 属性
  • FormMessage — Zod 校验错误自动展示

Crate C: form-validation

字段值
nameform-validation
typeutility
responsibilityZod 表单 Schema 工具集:从 @repo/schemas 派生表单 schema 的辅助函数、常用校验模式(required、minLength、email 等)、zodResolver 配置模板、null→undefined 兜底工具
metadata{"package": "zod", "version": "v4", "resolver": "@hookform/resolvers/zod"}

包含内容:

  • Zod 从 "zod/v4" 的统一导入
  • zodResolver 的标准配置
  • 从已有 schema .pick() / .omit() 的派生模式
  • 常用校验组合(如 requiredString、optionalString)
  • defaultValues 的 null → undefined 兜底工具

Crate D: form-store-bridge

字段值
nameform-store-bridge
typeutility
responsibility范式 B 的 react-hook-form ↔ Zustand 桥接层:createFormControl 在 slice 中的标准用法、handleFormSubmit 模式、formControl 与 slice 其他状态的生命周期协调
metadata{"package": "zustand", "version": "^5.x", "paradigm": "B"}

包含内容:

  • createFormControl&lt;T&gt;() 在 Zustand slice 中的标准位置
  • handleFormSubmit 的完整流程模板(mutate → toast → reset → close)
  • formControl 的 reset / 清理生命周期
  • 防止 formControl 引用泄漏的规范

目前项目中的实现分布

目标 Crate实际对应物位置类型
form-engineform-best-practice Skill.claude/skills/form-best-practice/Claude Code Skill
— references/checklist.md同上检查清单(范式 A/B 判定 + 7 节规则)
— references/form-best-practice-guide.md同上"两棵树"模型 + 状态隔离原则 + 两种范式
— references/anatomy.json同上表单组件树结构 JSON
— best-practice-examples/CreateUserForm.tsx同上范式 A 示例代码
form-best-practice Archetypedocs/archetypes/form-best-practice.mdDaedalus Archetype 文档
— 19 条 Condition(含 archetype_ref 传递依赖)同上结构化条件定义
form-componentsshadcn/ui Form 组件@/components/ui/form(标准 shadcn 位置)UI 组件
form-validation@repo/schemas(Zod schema 源)packages/schemas/src/共享 Schema 包
zod-infer-type-best-practice Skill.claude/skills/zod-infer-type-best-practice/Claude Code Skill
form-store-bridgeCrateDialog 迁移实战apps/app/src/实际组件(范式 B 的参考实现)
— useForm({ formControl }) 模式同上范式 B 的 formControl 托管
store-best-practice Skill.claude/skills/store-best-practice/Claude Code Skill(定义 createFormControl 规范)

Archetype 契约定义

Archetype ID: form-best-practiceScope: pageConcept: 确保所有前端表单遵循 react-hook-form + Zod + shadcn/ui 标准架构,实现表单状态与全局状态的严格隔离

> 我们这里假设出现的archetype_ref均已实现

C-1: 禁止 useState 管理表单字段
  • ID: c-form-no-usestate
  • 类型: text
  • 排序: 1
  • 依赖: 无
  • 条件内容:
    所有表单字段值必须由 react-hook-form 管理。禁止使用 useState、useReducer 或任何其他本地状态
    管理表单字段的 value/onChange。
    
    ❌ 违规: const [name, setName] = useState("")
    ✅ 正确: 通过 useForm({ defaultValues }) + <Input {...field} /> 管理
    
C-2: 禁止表单字段直接绑定 Zustand
  • ID: c-form-no-zustand-binding
  • 类型: text
  • 排序: 2
  • 依赖: 无
  • 条件内容:
    表单字段值禁止直接双向绑定到 Zustand 全局状态。禁止从 Zustand store 读取字段值并实时写回。
    
    ❌ 违规: <Input value={store.name} onChange={(e) => store.setName(e.target.value)} />
    ✅ 正确: 通过 react-hook-form field 管理,仅在 onSubmit 时将数据副本传给 store action
    
    注意:范式 B 下 formControl 实例托管在 slice 中不算违反此条(formControl 是控制器,不是字段值)。
    
C-3: 表单沙盒隔离
  • ID: c-form-sandbox
  • 类型: text
  • 排序: 3
  • 依赖: 无
  • 条件内容:
    表单是独立沙盒。所有用户输入仅在 react-hook-form 内部流转,不影响原始数据。
    仅在调用 onSubmit/handleSubmit 时,将表单值的深拷贝副本传递给外部(API、Zustand action 等)。
    
    ❌ 违规: 在 onChange 回调中实时 dispatch(setFormData(values))
    ✅ 正确: 在 handleSubmit((values) => onSubmit({ ...values })) 中传递副本
    
    关键原则:原始数据在整个表单生命周期内不可变(immutable),避免"边编辑边污染源数据"。
    
C-4: 范式 B — formControl 托管在 slice
  • ID: c-form-paradigm-b-formcontrol
  • 类型: archetype_ref
  • 排序: 4
  • 依赖: ["store-best-practice"]
  • 条件内容:
    范式 B 场景下,formControl 实例必须由 Zustand slice 通过 createFormControl<T>() 创建并持有。
    组件使用 useForm({ formControl: slice.formControl }) 接管,不在组件内本地创建 useForm()。
    
    ❌ 违规(范式 B 场景): 在组件内 const form = useForm({ resolver, defaultValues })
    ✅ 正确: slice 中 export const formControl = createFormControl<T>({...}),
            组件中 const form = useForm({ formControl: store.formControl })
    
C-5: 范式 B — handleFormSubmit 在 slice
  • ID: c-form-paradigm-b-submit
  • 类型: archetype_ref
  • 排序: 5
  • 依赖: ["store-best-practice"]
  • 条件内容:
    范式 B 场景下,完整的提交流程(mutate / navigate / toast / close)必须在 slice 的
    handleFormSubmit 方法中完成。组件仅通过 <form onSubmit={form.handleSubmit(handleFormSubmit)}>
    绑定,禁止在组件内 JS 主动调用 submit 逻辑。
    
    ❌ 违规(范式 B 场景): 组件内 const onSubmit = async (values) => { await mutate(...); close() }
    ✅ 正确: slice 中定义 handleFormSubmit,组件中只做绑定
    
C-6: shadcn/ui Form 外层包裹
  • ID: c-form-shadcn-wrapper
  • 类型: text
  • 排序: 6
  • 依赖: 无
  • 条件内容:
    表单外层必须使用 shadcn/ui 的 <Form {...form}> 组件包裹(展开 useForm 返回值)。
    禁止仅使用原生 <form> 元素而不使用 shadcn Form 组件。
    
    ❌ 违规: <form onSubmit={handleSubmit(...)}>  // 无 <Form>
    ✅ 正确: <Form {...form}><form onSubmit={form.handleSubmit(onSubmit)}>...</form></Form>
    
C-7: 完整字段层级结构
  • ID: c-form-field-hierarchy
  • 类型: text
  • 排序: 7
  • 依赖: 无
  • 条件内容:
    每个表单字段必须使用完整组件层级:
    FormField → FormItem → FormControl → 实际输入组件 → FormMessage
    
    ❌ 违规: 跳过 FormItem 直接 <FormControl><Input /></FormControl>
    ❌ 违规: 不使用 FormMessage 导致校验错误无显示
    ✅ 正确:
      <FormField control={form.control} name="xxx" render={({ field }) => (
        <FormItem>
          <FormLabel>XXX</FormLabel>
          <FormControl><Input {...field} /></FormControl>
          <FormMessage />
        </FormItem>
      )} />
    
C-8: {...field} 展开模式
  • ID: c-form-field-spread
  • 类型: text
  • 排序: 8
  • 依赖: 无
  • 条件内容:
    输入组件必须通过 render={({ field }) => <Input {...field} />} 方式接入 react-hook-form。
    严禁手动处理 value 和 onChange。
    
    ❌ 违规: <Input value={form.watch("name")} onChange={(e) => form.setValue("name", e.target.value)} />
    ✅ 正确: render={({ field }) => <Input {...field} />}
    
    注意:对于非原生输入组件(如 shadcn/ui 的 Select、DatePicker),应使用 <Controller>
    或该组件库提供的专用适配方式。
    
C-9: 提交按钮在 form 内部
  • ID: c-form-submit-button-inside
  • 类型: text
  • 排序: 9
  • 依赖: 无
  • 条件内容:
    <button type="submit">(或 shadcn/ui <Button type="submit">)必须位于它要提交的 <form> 元素内部。
    当表单内容和按钮拆分在不同 JSX 变量中时,确保 <form> 标签包裹两者。
    
    ❌ 违规:
      const formContent = <form>...</form>;
      const footer = <Button type="submit">Save</Button>;
      return <>{formContent}{footer}</>;  // button 在 form 外面
    
    ✅ 正确:
      const formContent = <div>...</div>;
      const footer = <Button type="submit">Save</Button>;
      return <form onSubmit={...}>{formContent}{footer}</form>;
    
C-10: Zod Schema 派生(禁止重复定义)
  • ID: c-form-zod-derivation
  • 类型: archetype_ref
  • 排序: 10
  • 依赖: ["zod-infer-type-best-practice"]
  • 条件内容:
    表单的 Zod schema 应优先从已有 schema(@repo/schemas)通过 .pick() / .omit() 派生,
    禁止在组件中重新手写字段定义和枚举值。
    
    ❌ 违规:
      const formSchema = z.object({
        name: z.string().min(1),
        type: z.enum(["package", "module", ...]),  // 与 @repo/schemas 重复
      });
    
    ✅ 正确:
      const formSchema = crateCreateSchema.pick({ name: true, type: true });
    
C-11: zodResolver 集成
  • ID: c-form-zod-resolver
  • 类型: text
  • 排序: 11
  • 依赖: 无
  • 条件内容:
    useForm 必须通过 zodResolver 集成 Zod Schema 校验。禁止使用自定义 validate 函数
    或手动 if/else 校验逻辑。
    
    ❌ 违规: useForm({ validate: (values) => { if (!values.name) return "Required" } })
    ✅ 正确: useForm({ resolver: zodResolver(formSchema) })
    
    导入规范:
    - z 必须从 "zod/v4" 导入(非 "zod")
    - zodResolver 必须从 "@hookform/resolvers/zod" 导入
    
C-12: Zod 从 zod/v4 导入
  • ID: c-form-zod-v4-import
  • 类型: text
  • 排序: 12
  • 依赖: 无
  • 条件内容:
    Zod 必须从 "zod/v4" 导入,禁止从 "zod" 导入。
    
    ❌ 违规: import { z } from "zod"
    ✅ 正确: import { z } from "zod/v4"
    
C-13: defaultValues 深拷贝
  • ID: c-form-defaultvalues-deepcopy
  • 类型: text
  • 排序: 13
  • 依赖: 无
  • 条件内容:
    useForm 的 defaultValues 必须使用原始数据的深拷贝副本,禁止直接传递引用对象。
    
    ❌ 违规: defaultValues: originalData  // 直接传引用
    ✅ 正确: defaultValues: { ...originalData }  // 浅拷贝副本
    ✅ 正确: defaultValues: structuredClone(originalData)  // 深拷贝副本(嵌套对象场景)
    
    原因:直接传引用会导致表单编辑时意外修改原始数据,破坏 immutable 原则。
    
C-14: null → undefined 处理
  • ID: c-form-null-undefined
  • 类型: text
  • 排序: 14
  • 依赖: 无
  • 条件内容:
    Drizzle 数据库可能返回 null 值,但 z.string().optional() 推导类型为 string | undefined。
    给 defaultValues 赋值时,对可能为 null 的字段使用 ?? "" 或 ?? undefined 兜底。
    
    ❌ 违规:
      defaultValues: {
        description: editing.description,  // DB 可能返回 null,类型不匹配
      }
    
    ✅ 正确:
      defaultValues: {
        description: editing.description ?? "",  // null → ""
      }
    
C-15: 提交时传递数据副本
  • ID: c-form-submit-deepcopy
  • 类型: text
  • 排序: 15
  • 依赖: 无
  • 条件内容:
    表单提交(onSubmit / handleSubmit 回调)时,必须将表单值副本传递给外部,
    禁止直接传递 react-hook-form 内部引用或原始对象引用。
    
    ❌ 违规: onSubmit(form.getValues())  // 传 react-hook-form 内部引用
    ✅ 正确: handleSubmit((values) => onSubmit({ ...values }))  // 传递副本
    
C-16: FormProvider 共享上下文
  • ID: c-form-provider-context
  • 类型: text
  • 排序: 16
  • 依赖: 无
  • 条件内容:
    当表单拆分为多个子组件时,必须使用 FormProvider 包裹以共享 react-hook-form 上下文。
    子组件通过 useFormContext() 访问 form 方法(control, setValue, getValues, watch 等)。
    
    ❌ 违规: 通过 props 将 control 或 register 逐层传递给子组件
    ✅ 正确:
      // 父组件
      const form = useForm();
      return <FormProvider {...form}><ChildComponent /></FormProvider>;
    
      // 子组件
      const { control } = useFormContext<FormValues>();
    
C-17: 禁止 control/register props 透传
  • ID: c-form-no-control-props
  • 类型: archetype_ref
  • 排序: 17
  • 依赖: ["c-form-provider-context"]
  • 条件内容:
    禁止通过组件 props 逐层传递 control 或 register。子组件需要访问 form 上下文时,
    必须使用 useFormContext()(配合父组件的 FormProvider)。
    
    ❌ 违规: function NameField({ control }: { control: Control<FormValues> })
    ✅ 正确: function NameField() { const { control } = useFormContext<FormValues>() }
    
    例外:原子级 UI 组件(如封装了 FormField 逻辑的通用字段组件)可以从 props 接收 control。
    
C-18: 命名导出函数组件
  • ID: c-form-named-export
  • 类型: text
  • 排序: 18
  • 依赖: 无
  • 条件内容:
    表单组件必须使用命名导出(named export),禁止使用默认导出(default export)。
    
    ❌ 违规: export default function MyForm()
    ✅ 正确: export const MyForm = function () { ... }
    
C-19: 范式 A — 标准 Props 接口
  • ID: c-form-paradigm-a-props
  • 类型: text
  • 排序: 19
  • 依赖: 无
  • 条件内容:
    范式 A(独立表单组件)必须接收以下标准 props:
    - initialData?: FormValues(可选,预填数据)
    - onSubmit: (data: FormValues) => void | Promise<void>(必填,提交回调)
    
    ❌ 违规: 使用其他命名如 data、defaultData、submitFn
    ✅ 正确: props 统一为 initialData 和 onSubmit
    
    Props 类型必须显式声明,禁止使用 any。
    

参考

飞书文档