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

如何在react-day-picker中禁用已预订及过往日期?

React-Day-Picker 预订组件禁用日期问题排查与修复

问题描述

我在React应用中使用react-day-picker开发预订组件,需求是禁用过往日期和已被预订的日期,但目前日历无法正确禁用已预订的未来日期,所有未来日期仍可被选择。

错误信息:无

环境版本

  • React: 17.0.2
  • react-day-picker: 8.0.0
  • axios: 0.21.1

当前组件代码

import { useContext, useEffect, useState } from "react";
import { differenceInCalendarDays, isSameDay } from "date-fns";
import axios from "axios";
import { Navigate, useParams } from "react-router-dom";
import { UserContext } from "../UserContext";
import { DayPicker } from "react-day-picker";
import "react-day-picker/dist/style.css";

function BookingWidget({ place }) {
  const [checkIn, setCheckIn] = useState("");
  const [checkOut, setCheckOut] = useState("");
  const [numberGuests, setNumberGuests] = useState(1);
  const [name, setName] = useState("");
  const [mobile, setMobile] = useState("");
  const [redirect, setRedirect] = useState("");
  const { user } = useContext(UserContext);
  const { id } = useParams();
  const [alreadyBooked, setAlreadyBooked] = useState([]);

  useEffect(() => {
    if (user) {
      setName(user.name);
    }
    axios.get("/booking/" + id).then((response) => {
      const bookings = response.data;
      const bookedDates = [];
      for (let booking of bookings) {
        const start = new Date(booking.checkIn);
        const end = new Date(booking.checkOut);
        for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {
          bookedDates.push(new Date(date));
        }
      }
      setAlreadyBooked(bookedDates);
    });
  }, [user, id]);

  let numberOfNights = 0;
  if (checkIn && checkOut) {
    numberOfNights = differenceInCalendarDays(
      new Date(checkOut),
      new Date(checkIn)
    );
  }

  async function bookThisPlace() {
    const response = await axios.post("/bookings", {
      checkIn,
      checkOut,
      numberGuests,
      name,
      mobile,
      price: numberOfNights * place.price,
      place: place._id,
    });
    const bookingId = response.data._id;
    setRedirect(`/account/bookings/${bookingId}`);
  }

  const isPastDay = (day) => {
    return day < new Date();
  };

  const isBookedDay = (day) => {
    return alreadyBooked.some(bookedDay => isSameDay(day, bookedDay));
  };

  const disabledDays = [
    { before: new Date() },  // Disable past days
    ...alreadyBooked.map(date => ({ date })) // Disable already booked days
  ];

  if (redirect) {
    return <Navigate to={redirect} />;
  }

  return (
    <div className="bg-white shadow p-4 rounded-2xl">
      <div className="text-2xl text-center mb-3">
        <b>Price: {place.price} €/night</b>
      </div>
      <div className="border mt-4 rounded-2xl">
        <div className="flex">
          <div className="py-3 px-4">
            <label>Check-in date: </label>
            <DayPicker
              mode="single"
              selected={checkIn ? new Date(checkIn) : undefined}
              onSelect={setCheckIn}
              disabled={disabledDays}
            />
          </div>
          <div className="py-3 px-4 border-l">
            <label>Check-out date: </label>
            <DayPicker
              mode="single"
              selected={checkOut ? new Date(checkOut) : undefined}
              onSelect={setCheckOut}
              disabled={disabledDays}
            />
          </div>
        </div>
        <div className="py-3 px-4 border-t">
          <label>Guests: </label>
          <input
            type="number"
            value={numberGuests}
            onChange={(ev) => setNumberGuests(ev.target.value)}
          />
        </div>
        {numberOfNights > 0 && (
          <div className="py-3 px-4 border-t">
            <label>Your name: </label>
            <input
              type="text"
              value={name}
              onChange={(ev) => setName(ev.target.value)}
            />
            <label>Phone: </label>
            <input
              type="tel"
              value={mobile}
              onChange={(ev) => setMobile(ev.target.value)}
            />
          </div>
        )}
      </div>

      <button onClick={bookThisPlace} className="primary rounded-2xl mt-4">
        Book now{" "}
        {numberOfNights > 0 && (
          <span>{numberOfNights * place.price} €</span>
        )}
      </button>
    </div>
  );
}

export default BookingWidget;

解决方案

1. 修复disabled属性的匹配逻辑

react-day-picker v8中,disabled属性接受的是日期匹配函数数组,而非直接的日期对象数组。直接使用{ date }会因为日期对象的引用差异导致匹配失效。

替换原disabledDays的定义,直接使用你已经实现的isPastDay和isBookedDay函数:

const disabledDays = [
  isPastDay,
  isBookedDay
];

2. 修正已预订日期数组的生成逻辑

原代码中循环生成已预订日期时,会修改同一个date对象并push到数组,导致数组内所有元素都是同一个对象的引用,后续匹配会出错。需要每次创建新的Date对象:

useEffect(() => {
  if (user) {
    setName(user.name);
  }
  axios.get("/booking/" + id).then((response) => {
    const bookings = response.data;
    const bookedDates = [];
    for (let booking of bookings) {
      const start = new Date(booking.checkIn);
      const end = new Date(booking.checkOut);
      // 复制start,避免修改原对象
      let currentDate = new Date(start);
      while (currentDate <= end) {
        bookedDates.push(new Date(currentDate)); // 每次创建新的日期对象
        currentDate.setDate(currentDate.getDate() + 1);
      }
    }
    setAlreadyBooked(bookedDates);
  });
}, [user, id]);

3. 优化日期状态的存储类型

当前checkIn和checkOut存储的是字符串,建议直接存储Date对象,避免频繁的类型转换:

// 初始化状态为null
const [checkIn, setCheckIn] = useState(null);
const [checkOut, setCheckOut] = useState(null);

// 更新DayPicker的属性
<DayPicker
  mode="single"
  selected={checkIn}
  onSelect={setCheckIn}
  disabled={disabledDays}
/>

// 计算入住天数时直接使用Date对象
let numberOfNights = 0;
if (checkIn && checkOut) {
  numberOfNights = differenceInCalendarDays(checkOut, checkIn);
}

// 提交时转换为字符串格式
async function bookThisPlace() {
  const response = await axios.post("/bookings", {
    checkIn: checkIn.toISOString(),
    checkOut: checkOut.toISOString(),
    numberGuests,
    name,
    mobile,
    price: numberOfNights * place.price,
    place: place._id,
  });
  const bookingId = response.data._id;
  setRedirect(`/account/bookings/${bookingId}`);
}

4. 可选:优化退房日期的禁用规则

通常退房日期需要晚于入住日期,可以给退房日历添加额外的禁用规则:

const disabledCheckOutDays = [
  isPastDay,
  isBookedDay,
  day => checkIn && day <= checkIn // 禁用入住日期及之前的日期
];

// 退房日历使用该规则
<DayPicker
  mode="single"
  selected={checkOut}
  onSelect={setCheckOut}
  disabled={disabledCheckOutDays}
/>

内容的提问来源于stack exchange,提问作者Lluís Mañas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 21:57:05