关于 web-forms-react-hook-form
为 React 表单开发提供 React Hook Form 模式的实用技能,适用于处理表单状态、验证与复用字段。内容涵盖受控组件、重复字段组,以及减少不必要重新渲染的订阅方式。
主要功能
- 01带默认值和注册的类型化表单
- 02使用 Controller 集成受控组件
- 03通过 resolver 接入架构验证
- 04使用 useFieldArray 管理重复行
- 05缩小表单状态订阅范围
适用场景
- 01构建带验证的可访问表单
- 02集成自定义选择框或日期选择器
- 03管理可增删和排序的表单行
- 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.yamlreference.mdexamples/arrays.mdcontrolled-components.mdcore.mdform-options.mdperformance.mdvalidation.mdwizard.md详细介绍
React Hook Form 模式
快速指南:
register绑定原生输入控件,并使其保持非受控;Controller包装持有自身值的组件;useFieldArray用于管理可重复的行,并以field.id作为键。验证规则可以通过register规则提供,也可以通过resolver传入 schema。需要关注的是重新渲染:useWatch和useFormState订阅指定字段,而watch()和宽泛解构的formState会订阅整个表单。
详细资源:
- examples/core.md — 使用
register、错误显示和无障碍属性构建类型安全的表单 - examples/controlled-components.md — 用
Controller包装选择框、日期选择器和复选框组 - examples/validation.md — 通过
resolver接入 schema - examples/arrays.md — 使用
useFieldArray管理明细行并实时计算总计 - examples/performance.md — 隔离订阅、
FormStateSubscribe、useWatch的exact和compute - examples/form-options.md — 用于异步数据的
values属性、disabled、<Form />组件 - examples/wizard.md — 使用逐步
trigger()的多步骤表单 - reference.md — 决策树、API 表格、版本与功能对照
适用哪种方式
- 原生
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 无法表达的跨字段规则,例如密码匹配或日期范围。
模式 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>