React Hook Form结合Zod实现时间输入框的条件验证问题
解决React Hook Form + Zod实现条件时间验证的问题
核心问题分析
你当前的superRefine逻辑在勾选复选框后立即触发验证,不符合"仅在两个输入框有填写动作后才显示错误提示"的需求。需要调整校验触发条件,只有当复选框勾选,且用户已经输入了部分时间内容(from或to不为空)时,才执行必填和时间逻辑校验。
调整后的Zod Schema实现
修改hoursSchema的superRefine逻辑,增加判断条件,避免勾选后立刻报错:
import { z } from "zod"; const hoursSchema = z .object({ day: z.string(), checked: z.boolean(), from: z.tuple([z.number(), z.number()]).nullish(), to: z.tuple([z.number(), z.number()]).nullish(), }) .superRefine(({ from, to, checked }, ctx) => { // 未勾选复选框,直接跳过校验 if (!checked) return; // 关键:只有当from或to至少有一个被填写时,才触发后续校验 const hasTimeInput = from !== null || to !== null; if (!hasTimeInput) return; // 必填项校验 if (!from && !to) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请填写开始和结束时间", path: ["from"], // 指定错误关联到from输入框,方便RHF定位展示 }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请填写开始和结束时间", path: ["to"], }); return; } if (!from) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请填写开始时间", path: ["from"], }); return; } if (!to) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请填写结束时间", path: ["to"], }); return; } // 时间先后逻辑校验 const [fromHour, fromMinute] = from; const [toHour, toMinute] = to; if (fromHour > toHour || (fromHour === toHour && fromMinute > toMinute)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "结束时间不能早于开始时间", path: ["to"], }); return; } if (fromHour === toHour && fromMinute === toMinute) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "开始和结束时间不能相同", path: ["to"], }); } }); export const availableHoursSchema = z.object({ availability: z.array(hoursSchema), });
React Hook Form组件配合实现
结合RHF的Controller和状态控制,实现输入框禁用、错误展示的完整逻辑:
import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { availableHoursSchema } from "./your-schema-path"; import type { z } from "zod"; type FormData = z.infer<typeof availableHoursSchema>; // 初始化一周七天的默认数据 const initialAvailability = [ { day: "周一", checked: false, from: null, to: null }, { day: "周二", checked: false, from: null, to: null }, { day: "周三", checked: false, from: null, to: null }, { day: "周四", checked: false, from: null, to: null }, { day: "周五", checked: false, from: null, to: null }, { day: "周六", checked: false, from: null, to: null }, { day: "周日", checked: false, from: null, to: null }, ]; export default function AvailabilityForm() { const { control, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(availableHoursSchema), defaultValues: { availability: initialAvailability, }, mode: "onChange", // 输入变化时触发校验,符合"填写后显示错误"的需求 }); const onSubmit = (data: FormData) => { console.log("提交的可用时间:", data); // 这里添加提交逻辑 }; return ( <form onSubmit={handleSubmit(onSubmit)} className="availability-form"> {initialAvailability.map((dayItem, index) => ( <div key={dayItem.day} className="day-time-group"> {/* 复选框Controller */} <Controller name={`availability[${index}].checked`} control={control} render={({ field: { value, onChange } }) => ( <label className="day-checkbox"> <input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} /> <span>{dayItem.day}</span> </label> )} /> {/* 开始时间输入框 */} <Controller name={`availability[${index}].from`} control={control} render={({ field: { value, onChange } }) => { const isChecked = control._formValues.availability[index].checked; return ( <div className="time-input-group"> <span>开始时间:</span> <input type="number" placeholder="时" disabled={!isChecked} value={value ? value[0] : ""} onChange={(e) => { const hour = Math.max(0, Math.min(23, parseInt(e.target.value) || 0)); onChange(value ? [hour, value[1]] : [hour, 0]); }} min={0} max={23} /> <span>:</span> <input type="number" placeholder="分" disabled={!isChecked} value={value ? value[1] : ""} onChange={(e) => { const minute = Math.max(0, Math.min(59, parseInt(e.target.value) || 0)); onChange(value ? [value[0], minute] : [0, minute]); }} min={0} max={59} /> {/* 展示开始时间的错误 */} {errors.availability?.[index]?.from && ( <p className="error-text">{errors.availability[index].from.message}</p> )} </div> ); }} /> {/* 结束时间输入框 */} <Controller name={`availability[${index}].to`} control={control} render={({ field: { value, onChange } }) => { const isChecked = control._formValues.availability[index].checked; return ( <div className="time-input-group"> <span>结束时间:</span> <input type="number" placeholder="时" disabled={!isChecked} value={value ? value[0] : ""} onChange={(e) => { const hour = Math.max(0, Math.min(23, parseInt(e.target.value) || 0)); onChange(value ? [hour, value[1]] : [hour, 0]); }} min={0} max={23} /> <span>:</span> <input type="number" placeholder="分" disabled={!isChecked} value={value ? value[1] : ""} onChange={(e) => { const minute = Math.max(0, Math.min(59, parseInt(e.target.value) || 0)); onChange(value ? [value[0], minute] : [0, minute]); }} min={0} max={59} /> {/* 展示结束时间的错误 */} {errors.availability?.[index]?.to && ( <p className="error-text">{errors.availability[index].to.message}</p> )} </div> ); }} /> </div> ))} <button type="submit" className="submit-btn">提交可用时间</button> </form> ); }
关键优化点说明
- 校验触发条件调整:通过
hasTimeInput判断,只有用户填写了至少一个时间输入框时才触发校验,避免勾选复选框后立刻报错。 - 错误路径绑定:在Zod的
addIssue中指定path,让错误信息精准关联到对应的输入框,方便RHF正确展示。 - 输入状态控制:根据复选框状态控制时间输入框的禁用状态,初始全部禁用,勾选后才允许输入。
- 输入值边界处理:对时间输入做了0-23(小时)、0-59(分钟)的边界限制,避免无效值进入校验逻辑。
内容的提问来源于stack exchange,提问作者rudzky
相关产品推荐
相关产品推荐

