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

React Hook Form中number类型字段添加占位符的问题(含Zod校验)

React Hook Form + Zod 数组字段默认值与占位符冲突解决

问题背景

Zod Schema 定义

const SecondFormSchema = z.object({
  trainingType: z.enum(["A", "B", "C", "D", "E", "FullBody"]),
  trainingName: z.string().min(2),
  exercises: z
    .array(
      z.object({
        coachNotes: z.string().min(2),
        repsNumber: z.number().int().min(1),
        setsNumber: z.number().int().min(1),
        restTime: z.optional(z.number().int().min(1)),
        exerciseDetails: z.string(),
      }),
    )
    .nonempty({ message: "Please add at least one exercise" }),
});

当前添加数组项的代码

<Button
  type="button"
  onClick={() =>
    append({
      coachNotes: "",
      repsNumber: 0,
      setsNumber: 0,
      restTime: 0,
      exerciseDetails: "",
    })
  }
>
  Add Exercise
</Button>

核心问题

使用append添加新exercise项时,必须传入默认值,但repsNumber、setsNumber、restTime设置为0后,输入框显示0而非预设的占位符。尝试用form.setValue设为undefined,但Zod Schema限制这些字段为数字类型,无法生效。


解决方案

方案1:调整Zod Schema,允许初始undefined并强化提交校验

修改必填数字字段的Schema,允许初始值为undefined,同时通过数组refine确保提交时每个exercise项的必填数字字段都有有效值:

const SecondFormSchema = z.object({
  trainingType: z.enum(["A", "B", "C", "D", "E", "FullBody"]),
  trainingName: z.string().min(2),
  exercises: z
    .array(
      z.object({
        coachNotes: z.string().min(2),
        // 允许初始undefined,提交时校验必填
        repsNumber: z.union([z.undefined(), z.number().int().min(1)]),
        setsNumber: z.union([z.undefined(), z.number().int().min(1)]),
        restTime: z.optional(z.number().int().min(1)),
        exerciseDetails: z.string(),
      }),
    )
    .nonempty({ message: "Please add at least one exercise" })
    // 确保每个exercise的reps和sets都已填写
    .refine(
      (exercises) => exercises.every(ex => ex.repsNumber !== undefined && ex.setsNumber !== undefined),
      { message: "Reps and sets are required for each exercise" }
    ),
});

然后修改append的默认值为undefined:

<Button
  type="button"
  onClick={() =>
    append({
      coachNotes: "",
      repsNumber: undefined,
      setsNumber: undefined,
      restTime: undefined,
      exerciseDetails: "",
    })
  }
>
  Add Exercise
</Button>

此时输入框值为undefined,会正常显示占位符,提交时Zod会拦截未填写的项。

方案2:自定义输入组件,处理0值显示占位符

如果不想修改Schema,可以自定义数字输入组件,将0值转换为空字符串显示,同时保留数字类型的校验逻辑:

import { useFormContext } from "react-hook-form";

const NumberInput = ({ name, placeholder, ...props }) => {
  const { register } = useFormContext();
  const { ref, onChange, value } = register(name);

  const handleChange = (e) => {
    // 空输入转为0,确保Zod能识别为数字类型
    const inputVal = e.target.value === "" ? 0 : parseInt(e.target.value, 10);
    onChange({ target: { name, value: inputVal } });
  };

  return (
    <input
      type="number"
      ref={ref}
      onChange={handleChange}
      // 0值显示为空,触发占位符
      value={value === 0 ? "" : value}
      placeholder={placeholder}
      {...props}
    />
  );
};

使用时直接替换原输入框:

<NumberInput name={`exercises[${index}].repsNumber`} placeholder="Enter reps" />
<NumberInput name={`exercises[${index}].setsNumber`} placeholder="Enter sets" />

这种方式不需要修改append的默认值,输入框会自动把0显示为占位符,用户输入后转为数字。

方案3:用Controller组件做值转换

借助React Hook Form的Controller组件,手动控制输入值的显示和提交转换:

import { Controller } from "react-hook-form";

// 在循环渲染exercise项时使用
<Controller
  name={`exercises[${index}].repsNumber`}
  control={control}
  render={({ field: { onChange, value } }) => (
    <input
      type="number"
      onChange={(e) => {
        const val = e.target.value === "" ? 0 : parseInt(e.target.value, 10);
        onChange(val);
      }}
      value={value === 0 ? "" : value}
      placeholder="Enter reps"
    />
  )}
/>

此方案和方案2逻辑类似,适合需要更精细控制表单字段的场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 08:57:17