You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React-Hook-Form搭配Zod提交表单时无法返回全部验证错误

D&D角色表表单提交验证异常排查与解决

问题现象

  • 所有字段留空时,首次提交errors对象为空;第二次提交仅返回Level字段错误,其余必填字段无报错,不符合预期(预期所有必填字段均返回对应错误)。
  • 字段失焦(Blur)时验证功能正常,比如未选择Class下拉框、HP设为负数等场景,能生成正确的错误信息。

相关代码配置

useForm 配置

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onBlur',
  defaultValues: schemaDefaults
});

Zod 校验 Schema

const invalidNumberError = '必须输入数字'

const pureNumberValidation = z.number({ invalid_type_error: invalidNumberError });
const scoreValidation = z.number({ invalid_type_error: invalidNumberError }).positive().min(1).max(30);
const positiveNumberValidation = z.number({ invalid_type_error: invalidNumberError }).positive();

export const schema = z.object({
  name: z.string().nonempty({ message: '姓名必填' }),
  class: pureNumberValidation.min(1, { message: '职业必填' }),
  level: positiveNumberValidation.max(20),
  hp: positiveNumberValidation,
  ac: positiveNumberValidation,
  speed: positiveNumberValidation,
  initiative: positiveNumberValidation,
  proficiencyBonus: positiveNumberValidation,
  race: pureNumberValidation.min(1, { message: '种族必填' }),
  alignment: pureNumberValidation.min(1, { message: '阵营必填' }),
  constitutionScore: scoreValidation,
  constitutionProficiencyBonus: pureNumberValidation,
  charismaScore: scoreValidation,
  charismaProficiencyBonus: pureNumberValidation,
  dexterityScore: scoreValidation,
  dexterityProficiencyBonus: pureNumberValidation,
  intelligenceScore: scoreValidation,
  intelligenceProficiencyBonus: pureNumberValidation,
  strengthScore: scoreValidation,
  strengthProficiencyBonus: pureNumberValidation,
  wisdomScore: scoreValidation,
  wisdomProficiencyBonus: pureNumberValidation,
  acrobatics: pureNumberValidation,
  animalhandling: pureNumberValidation,
  arcana: pureNumberValidation,
  athletics: pureNumberValidation,
  deception: pureNumberValidation,
  history: pureNumberValidation,
  insight: pureNumberValidation,
  intimidation: pureNumberValidation,
  investigation: pureNumberValidation,
  medicine: pureNumberValidation,
  nature: pureNumberValidation,
  perception: pureNumberValidation,
  performance: pureNumberValidation,
  persuasion: pureNumberValidation,
  religion: pureNumberValidation,
  sleightofhand: pureNumberValidation,
  stealth: pureNumberValidation,
  survival: pureNumberValidation
});

提交错误输出

{level: {…}}
level:
  message: "数字必须大于0"
  ref: input#level-input.<tailwind css classes>
  type: "too_small"

复用数字输入组件

<div>
  <label
    id={id}
    htmlFor={`${id}-input`}
    aria-label={label}
    className="<classes>"
  >
    {label}
  </label>
  <input
    type="number"
    id={`${id}-input`}
    {...register(name, { valueAsNumber: true })}
    min={min}
    max={max}
    aria-invalid={errors[name]?.message ? true : false}
    className='<classes>'
  />
  {errors[name]?.message && <ErrorDisplay message={errors[name].message} />}
</div>

问题原因

  1. useForm配置mode: 'onBlur'时,React Hook Form默认仅验证**已触摸(用户点击并离开过)**的字段,提交时不会自动校验未触摸的字段。
  2. 首次提交时所有字段均未被触摸,因此Zod未执行校验,errors为空;第二次提交时Level字段可能被意外触摸(如点击过输入框),仅该字段被校验并返回错误。
  3. 数字输入框为空时,valueAsNumber会将空值转为NaN,但未触摸的字段不会触发Zod的类型校验。

解决方案

方法1:提交时手动触发全字段验证

在提交函数中调用trigger()强制校验所有字段,确保未触摸的字段也被验证:

const {
  register,
  handleSubmit,
  formState: { errors },
  trigger, // 引入trigger方法
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onBlur',
  defaultValues: schemaDefaults
});

const onSubmit = async (data: FormData) => {
  // 触发所有字段的验证
  const isValid = await trigger();
  if (!isValid) return;
  
  // 后续提交逻辑
  console.log('表单数据:', data);
};

方法2:修改useForm配置,强制提交时验证所有字段

调整useForm的验证模式,保留onBlur触发验证的同时,开启提交时全字段校验:

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onBlur',
  reValidateMode: 'onSubmit', // 提交时重新验证所有字段
  defaultValues: schemaDefaults
});

方法3:优化Zod Schema处理空输入场景

针对数字输入框的空值问题,使用z.coerce.number()替代z.number(),将空字符串强制转为NaN,确保即使字段未被触摸,Zod也能捕获类型错误:

// 修改数字校验规则
const pureNumberValidation = z.coerce.number({ invalid_type_error: invalidNumberError });
const scoreValidation = z.coerce.number({ invalid_type_error: invalidNumberError }).positive().min(1).max(30);
const positiveNumberValidation = z.coerce.number({ invalid_type_error: invalidNumberError }).positive();

内容的提问来源于stack exchange,提问作者user1779418

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 04:25:55