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

