使用ZOD与React验证日期:确保开始日期早于结束日期
我正在开发一个度假应用的表单,用来让用户添加度假信息,用Zod做验证。规则是:
- 开始日期不能是过去的日期
- 结束日期不能等于或早于开始日期
我尝试给endDate加refine方法,但拿不到startDate的值,请问怎么实现这个验证?
注:我的Vacation.ts里已经有基础验证:
this.startDate = startDate < new Date() ? (() => { throw new Error("Start date cannot be earlier than today."); })() : startDate; this.endDate = endDate < startDate ? (() => { throw new Error("End date cannot be earlier than start date."); })() : endDate;
但我希望在表单提交前通过onBlur实现动态验证,用户选日期时就即时触发。我还考虑用MUI的范围日期选择器,但不确定能不能解决问题。以下是我的表单代码:
function AddVacation(): JSX.Element { let allVacations: Vacation[] = []; type FormData = { // id: number, destination: string, startDate: Date, endDate: Date, vacDesc: string, vacPrice: number, vacImg: string, } // This will be the schema for the form, it'll be used to validate the form when submitting. //z.coerce.date() is used to convert the string to a date object. const schema: ZodType<FormData> = z.object({ destination: z.string().min(2, {message: 'Destination must have at least 2 characters'}).nonempty(), startDate: z.coerce.date().refine((data) => data > new Date(), {message: 'Start date must be in the future'}), endDate: z.coerce.date(), vacDesc: z.string().min(2).nonempty(), vacPrice: z.number().positive().max(1000), vacImg: z.string().nonempty() }) const navigate = useNavigate(); const { register, handleSubmit, trigger ,formState: { errors } } = useForm<FormData>({resolver: zodResolver(schema)}); const onSubmit = (data: FormData) => { console.log("submitting...") console.log(errors); const newVacation = new Vacation(data.destination, data.startDate, data.endDate, data.vacDesc, data.vacPrice, data.vacImg); allVacations.push(newVacation); console.log(data); console.log(`allVacations: ${allVacations}`) navigate("/vacationList") }
1. 修改Zod Schema,添加全局Refine验证
要在验证endDate时获取startDate的值,不能单独给endDate加字段级的refine,得给整个object加全局refine,这样就能拿到整个表单的数据对象,同时校验两个日期:
const schema: ZodType<FormData> = z.object({ destination: z.string().min(2, {message: '目的地至少需要2个字符'}).nonempty(), startDate: z.coerce.date().refine(data => data > new Date(), {message: '开始日期必须是未来的日期'}), endDate: z.coerce.date(), vacDesc: z.string().min(2).nonempty({message: '假期描述不能为空'}), vacPrice: z.number().positive().max(1000, {message: '价格不能超过1000'}), vacImg: z.string().nonempty({message: '图片链接不能为空'}) }).refine( (data) => data.endDate > data.startDate, { message: '结束日期必须晚于开始日期', path: ['endDate'] // 指定错误提示绑定到endDate字段上 } );
这里的关键是用z.object().refine()而非字段级refine,这样能访问整个表单的data对象,对比startDate和endDate。同时通过path参数把错误信息关联到endDate字段,让错误提示正确显示在对应输入框下方。
2. 实现onBlur即时验证
要让用户选完日期就触发验证,只需在注册表单字段时,给startDate和endDate添加onBlur事件,调用react-hook-form的trigger方法:
// 注册startDate字段 <input {...register("startDate")} type="date" onBlur={() => trigger("startDate")} /> {errors.startDate && <span style={{color: 'red'}}>{errors.startDate.message}</span>} // 注册endDate字段 <input {...register("endDate")} type="date" onBlur={() => trigger(["startDate", "endDate"])} // 修改endDate时可同时触发startDate验证,确保起始日期有效 /> {errors.endDate && <span style={{color: 'red'}}>{errors.endDate.message}</span>}
这样用户选完日期离开输入框时,就会立刻触发验证并显示错误提示,不用等到提交表单。
3. 关于MUI范围日期选择器的建议
MUI的DateRangePicker确实能从交互层面避免用户选无效日期范围——它会限制结束日期不能早于开始日期,能减少前端验证的压力。但即使使用范围选择器,仍建议保留Zod验证作为兜底,防止用户通过控制台篡改数据。
如果用DateRangePicker,需要把返回的日期范围拆成startDate和endDate存入表单状态:
import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { TextField, Box } from '@mui/material'; import { setValue } from 'react-hook-form'; // 组件内部 const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null]); // 处理日期范围变化 const handleDateRangeChange = (newRange: [Dayjs | null, Dayjs | null]) => { setDateRange(newRange); // 更新react-hook-form的值 setValue("startDate", newRange[0]?.toDate() || null); setValue("endDate", newRange[1]?.toDate() || null); // 触发验证 trigger(["startDate", "endDate"]); }; // 渲染组件 <LocalizationProvider dateAdapter={AdapterDayjs}> <DateRangePicker startText="开始日期" endText="结束日期" value={dateRange} onChange={handleDateRangeChange} renderInput={(startProps, endProps) => ( <> <TextField {...startProps} /> <Box sx={{ mx: 2 }}>至</Box> <TextField {...endProps} /> </> )} /> </LocalizationProvider>
这样既优化了用户体验,又保留了严格的验证逻辑。
内容的提问来源于stack exchange,提问作者DanWi

