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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 21:05:39