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
相关产品推荐
相关产品推荐

