mcpField
W
web-forms-react-hook-form

web-forms-react-hook-form

github.com/agents-inc开发2660 次安装2 天前更新
代码仓库

关于 web-forms-react-hook-form

为 React 表单开发提供 React Hook Form 模式的实用技能,适用于处理表单状态、验证与复用字段。内容涵盖受控组件、重复字段组,以及减少不必要重新渲染的订阅方式。

主要功能

  1. 01带默认值和注册的类型化表单
  2. 02使用 Controller 集成受控组件
  3. 03通过 resolver 接入架构验证
  4. 04使用 useFieldArray 管理重复行
  5. 05缩小表单状态订阅范围

适用场景

  1. 01构建带验证的可访问表单
  2. 02集成自定义选择框或日期选择器
  3. 03管理可增删和排序的表单行
  4. 04减少大型表单的重复渲染

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-forms-react-hook-form

安装后在 Claude Code 中输入/web-forms-react-hook-form确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
arrays.md
controlled-components.md
core.md
form-options.md
performance.md
validation.md
wizard.md

详细介绍

React Hook Form 模式

快速指南:register 绑定原生输入控件,并使其保持非受控;Controller 包装持有自身值的组件;useFieldArray 用于管理可重复的行,并以 field.id 作为键。验证规则可以通过 register 规则提供,也可以通过 resolver 传入 schema。需要关注的是重新渲染:useWatch 和 useFormState 订阅指定字段,而 watch() 和宽泛解构的 formState 会订阅整个表单。

详细资源:


适用哪种方式

  • 原生 input、select 或 textarea — register 将 ref 交给表单,字段保持非受控,输入时不会触发重新渲染。请参阅 examples/core.md。
  • 自行管理值的组件 — 自定义选择框、日期选择器或富文本编辑器不提供可用的 ref,因此 Controller 会提供 value 和 onChange,并将重新渲染限制在该字段。请参阅 examples/controlled-components.md。
  • 以 schema 而非 register 规则声明的验证 — 通过 resolver 传入 schema,字段级 rules 即可省去。请参阅 examples/validation.md。

<critical_requirements>

编写 React Hook Form 代码前

调用 useForm<FormData>() 时,使用泛型,并为每个字段设置 defaultValues。 泛型为每个字段路径和提交载荷指定类型;默认值则确保每个输入从首次渲染开始就是受控的。

将 useFieldArray 的行键设为 field.id。 这是 RHF 分配给行的标识,在添加、删除和重新排序时都会保留,而数组索引不会。

组件一旦持有自己的值,就使用 Controller。 register 需要一个指向原生输入的 ref,而不转发 ref 的组件无法加入表单。

将 mode 设为 "onBlur" 或 "onTouched"。 默认值 "onSubmit" 会延迟反馈,直到首次提交;而 "onChange" 则会在每次按键时验证。

通过 resolver 传入 schema 验证,并将 schema 放在单独的模块中。 独立于组件的 schema 可以单独测试,也能在多个表单间复用;表单只需负责接线。

</critical_requirements>


自动检测: react-hook-form、useForm、register、handleSubmit、formState、Controller、 useFieldArray、useWatch、useFormContext、useFormState、FormProvider、FormStateSubscribe、SubmitHandler、 resolver、shouldUnregister、valueAsNumber

适用于:

  • React 中的表单状态、提交和验证接线
  • 可添加、删除和重新排序的可重复字段组
  • 持有自身值且需要接入表单的组件
  • 一个表单跨多个屏幕的多步流程
  • 减少字段较多的表单中的重新渲染

其他内容:

  • 编写验证 schema — resolver 接受 schema 对象,本指南只负责将其接入;schema 如何声明规则由其所属方决定。
  • 初始值的来源 — 表单通过 prop 或 values 选项接收初始值,不会自行获取数据。
  • 输入项的标记和样式 — 所有示例都使用普通元素,因此类名由你决定。

表单状态存储在 React 状态之外。输入项会向表单注册并通过 ref 报告变化,因此按键会更新表单自身的存储,而不会重新渲染持有该字段的组件。所有读取表单状态的内容——错误消息、计算得出的总计、提交按钮——都通过订阅指定的部分加入更新;没有订阅任何内容的组件则不会重新渲染。

因此,读取范围过广会带来很大开销。在渲染函数体中调用 watch(),以及解构出六个属性的 formState,都会订阅整个表单,从而抵消该库旨在实现的隔离效果。


核心模式

模式 1:使用泛型的 useForm

泛型、mode 和 defaultValues 共同决定类型安全、验证时机和受控输入警告。

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
} = useForm<ContactFormData>({
  mode: "onBlur",
  defaultValues: { name: "", email: "", message: "" },
});

完整代码:examples/core.md


模式 2:为持有自身值的组件使用 Controller

Controller 会渲染字段本身,并向其传入 value 和 onChange。选择依据是:ref 转发到原生输入的组件可以使用 register,其他组件则需要 Controller。

<Controller
  name="service"
  control={control}
  rules={{ required: "Service is required" }}
  render={({ field, fieldState: { error } }) => (
    <>
      <Select {...field} options={serviceOptions} />
      {error && <span role="alert">{error.message}</span>}
    </>
  )}
/>

完整代码:examples/controlled-components.md


模式 3:使用 useFieldArray 创建可重复行

fields 会为每行生成一个 id,并将其用作 React 键。数组级规则设置在 hook 上,相关错误会出现在 errors.items.root。

const { fields, append, remove } = useFieldArray({ control, name: "items" });

{fields.map((field, index) => (
  <div key={field.id}>
    <input {...register(`items.${index}.name`)} />
    <button type="button" onClick={() => remove(index)}>Remove</button>
  </div>
))}

完整代码:examples/arrays.md


模式 4:通过 resolver 进行 schema 验证

resolver 会取代逐字段的 rules:schema 决定哪些内容有效,并针对字段路径报告错误;表单负责接线。

const { register, handleSubmit } = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: "onBlur",
  defaultValues: { username: "", email: "" },
});

将 schema 放在单独的模块中,可以在不渲染组件的情况下进行测试,也能在多个表单间复用,还可以声明逐字段 rules 无法表达的跨字段规则,例如密码匹配或日期范围。

完整代码:examples/validation.md


模式 5:使用 useWatch 获取派生值

子组件中的 useWatch 会订阅指定字段,因此这些字段变化时,只有该子组件会重新渲染。

function PriceDisplay({ control }: { control: Control<PricingFormData> }) {
  const [plan, seats] = useWatch({ control, name: ["plan", "seats"] });
  return <div>Total: ${PLAN_PRICES[plan] * seats}</div>;
}

compute 选项可以进一步缩小订阅范围——只有计算结果发生变化时,组件才会重新渲染,而不是在输入值变化时就重新渲染。

完整代码:examples/performance.md 模式 11 和模式 12


模式 6:使用 useFormContext 处理嵌套字段

FormProvider 会将表单方法放入上下文中,以便嵌套或复用的区块无需逐层传递属性即可访问这些方法。嵌套达到三层,或某个区块需要渲染多次时,使用它会更方便;层级更浅时,直接传递 control 更简单。

<FormProvider {...methods}>
  <form onSubmit={methods.handleSubmit(onSubmit)}>
    <AddressFields prefix="shippingAddress" />
    <AddressFields prefix="billingAddress" />
  </form>
</FormProvider>

function AddressFields({ prefix }) {
  const { register } = useFormContext<CheckoutFormData>();
  return <input {...register(`${prefix}.street`)} />;
}

完整代码:examples/wizard.md


模式 7:加载外部数据

values 是响应式的——表单会随数据变化而更新;而 defaultValues 只会在挂载时读取一次。将 values 与 resetOptions: { keepDirtyValues: true } 配合使用,这样后台刷新就不会丢弃用户已输入的内容。

useForm<FormData>({
  values: userData,
  resetOptions: { keepDirtyValues: true },
});

成功保存后,reset(data) 会同时替换值和默认值,从而清除 isDirty。不带参数的 reset() 会恢复为原始默认值,可用于取消按钮。

完整代码:examples/form-options.md 模式 7


模式 8:隔离错误显示

带有 name 的 useFormState 只有在该字段的状态变化时才会触发重新渲染,从而避免错误消息导致其所在的整个表单重新渲染。

function FieldError<T extends FieldValues>({ control, name }: Props<T>) {
  const { errors } = useFormState({ control, name });
  const error = errors[name];
  if (!error) return null;
  return <span role="alert">{error.message as string}</span>;
}

完整代码:examples/performance.md — 钩子见模式 5,FormStateSubscribe 组件形式见模式 10


<red_flags>

注意事项

会导致运行时问题:

  • 将数组索引用作 useFieldArray 的键——React 会匹配到错误的行,因此移除中间项时,其下方的所有值都会上移一位。应使用 field.id 作为键。
  • 在自行持有值的组件上使用 register——组件不会收到 ref,字段无法注册,其值也不会出现在提交数据中。应使用 Controller 包装。
  • 未提供 defaultValues——输入框会以非受控状态挂载,并在首次按键时切换为受控状态;React 会对此发出警告,而 SSR 会将其报告为水合不匹配。为每个字段提供初始值,包括 ""。
  • 传给 append、prepend 或 insert 的对象不完整——缺失的键会变为 undefined,对应的输入框会处于非受控状态。请传入完整的条目。
  • 在 onSubmit 内抛出错误——handleSubmit 不会捕获该错误,因此拒绝会未处理地冒出。请在回调内部捕获错误。
  • 对字段数组自身的名称调用 setValue——行 ID 不会随值移动。请使用 replace()。

容易出乎意料的行为:

  • useForm 未指定泛型时,字段路径和提交数据的类型会变为 any,因此 register("emial") 这样的拼写错误也会被默默接受。
  • 解构多个 formState 属性会订阅所有这些属性,导致表单在任何属性变化时重新渲染。只获取组件会读取的属性,或使用 useFormState 将其隔离。
  • 在渲染函数体中使用 watch() 会订阅所有字段;在子组件中使用 useWatch 则可将其缩小为指定字段。
  • 调用 setValue 时不设置 shouldValidate: true,屏幕上会保留之前的错误。
  • 数组级错误位于 errors.items.root;单项错误位于 errors.items[index].field。
  • shouldUnregister: true 会丢弃已卸载字段的值。对于会隐藏字段的场景(尤其是向导),请保持为 false(默认值)。
  • useWatch 在首次渲染时会返回其 defaultValue,此时订阅尚未建立。
  • values 用于持续变化的外部数据,defaultValues 用于静态初始值;同时提供两者时,哪一个生效取决于 resetOptions。
  • 分步验证需要调用 trigger(fieldNames)——isValid 反映的是整个表单的状态,因此依赖它来控制能否进入下一步的向导会卡在第一步。

常见问题的完整前后对比代码见 reference.md。

</red_flags>

评论

支持 Markdown