React Big Calendar中onRangeChange无法更新backgroundEvents问题
解决React Big Calendar中onRangeChange更新backgroundEvents时日历无法切换的问题
问题原因
当在onRangeChange钩子中更新组件state(如backgroundEvents)时,组件会重新渲染。由于你使用的是非受控模式(仅依赖defaultView),日历会重置回初始的当前周范围,导致无法切换到新的周/日视图。
解决方案
将日历改为受控模式,手动维护当前显示的日期,并修正日期循环中的潜在问题:
1. 添加受控状态
新增currentDate state来控制日历的当前显示日期:
const [currentDate, setCurrentDate] = useState(new Date());
2. 修改Calendar组件属性
给DnDCalendar添加date属性绑定currentDate:
<DnDCalendar // 原有属性保留... date={currentDate} // 新增受控日期属性 />
3. 更新handleRangeChange逻辑
在handleRangeChange中,不仅更新backgroundEvents,还要同步更新currentDate为新范围的起始日期:
const handleRangeChange = (range) => { const dateRange = Array.isArray(range) ? range : [range.start, range.end]; console.log("Date Range:", dateRange); // 更新当前显示日期为新范围的起始 setCurrentDate(new Date(dateRange[0])); // 生成并更新背景事件 const newBackgroundEvents = getBackgroundEvents(dateRange); setBackgroundEvents(newBackgroundEvents); };
4. 修正getBackgroundEvents中的日期循环问题
原代码中直接修改循环变量date会导致日期比较异常,需每次创建新的Date对象:
const getBackgroundEvents = (range) => { let events = []; // 复制起始日期,避免修改原对象 let currentDate = new Date(range[0]); while (currentDate <= range[1]) { // Adjusting so that Monday = 0, Tuesday = 1, ..., Sunday = 6 const day = (currentDate.getDay() + 6) % 7; const formattedDate = formatISO(currentDate, { representation: "date" }); const specialHours = specialOpeningHours.filter((special) => special.open.startsWith(formattedDate) ); let hours = specialHours.length > 0 ? specialHours : openingHours[day]; hours.forEach((slot) => { let start, end; if (specialHours.length > 0) { start = parseISO(slot.open); end = parseISO(slot.close); } else { start = parseISO(`${formattedDate}T${slot.open}`); end = parseISO(`${formattedDate}T${slot.close}`); } events.push({ start, end }); }); // 创建新的日期对象,避免修改循环变量 currentDate = new Date(currentDate); currentDate.setDate(currentDate.getDate() + 1); } return events; };
修改后的完整代码
import "../../../styles/components/scheduler.scss"; import React, { useEffect, useState, useCallback, useRef } from "react"; import { getISOWeek, addYears, subYears, parseISO, isSameDay, formatISO } from "date-fns"; import { Calendar, dateFnsLocalizer, Views, CalendarProps } from "react-big-calendar"; import format from "date-fns/format"; import parse from "date-fns/parse"; import startOfWeek from "date-fns/startOfWeek"; import endOfWeek from "date-fns/endOfWeek"; import getDay from "date-fns/getDay"; import de from "date-fns/locale/de"; import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop/withDragAndDrop"; const openingHours = { 0: [ { open: "04:00", close: "12:00" }, { open: "13:00", close: "17:45" } ], 1: [{ open: "05:00", close: "17:45" }], 2: [{ open: "06:00", close: "17:45" }], 3: [{ open: "08:00", close: "17:45" }], 4: [ { open: "06:30", close: "12:00" }, { open: "13:30", close: "23:00" } ], 5: [{ open: "08:00", close: "17:45" }], 6: [], }; const specialOpeningHours = [ { open: "2024-01-05T05:00:00", close: "2024-01-05T12:00:00" }, { open: "2024-01-05T13:00:00", close: "2024-01-05T23:59:00" }, ]; const myEventsList = [ { title: "My Event", start: parseISO("2024-01-07T13:05:00"), end: parseISO("2024-01-07T14:00:00") }, { title: "My Event 2", start: parseISO("2024-01-07T13:35:00"), end: parseISO("2024-01-07T14:30:00") }, { title: "My Event 3", start: parseISO("2024-01-12T13:35:00"), end: parseISO("2024-01-12T14:30:00") }, ]; function Scheduler() { const [backgroundEvents, setBackgroundEvents] = useState([]); const [currentDate, setCurrentDate] = useState(new Date()); // 新增受控日期状态 const clickRef = useRef(null); const getCurrentWeekRange = () => { const today = new Date(); const start = startOfWeek(today, { weekStartsOn: 0 }); const end = endOfWeek(today, { weekStartsOn: 0 }); return [start, end]; }; useEffect(() => { const currentWeekRange = getCurrentWeekRange(); handleRangeChange(currentWeekRange); }, []); useEffect(() => { return () => { window.clearTimeout(clickRef?.current); }; }, []); const DnDCalendar = withDragAndDrop(Calendar); const locales = { de: de }; const messages = { week: "Woche", work_week: "Arbeitswoche", day: "Tag", month: "Monat", previous: "zurück", next: "vor", today: "Heute", agenda: "Liste", showMore: (total) => `+${total} más`, }; const localizer = dateFnsLocalizer({ format, parse, startOfWeek, getDay, locales }); function updateEvent(event) { console.log(event); console.log(event.start.toUTCString()); } function newEvent(event) { console.log(event); } const onSelectEvent = useCallback((calEvent) => { console.log("onSelectEvent"); window.clearTimeout(clickRef?.current); clickRef.current = window.setTimeout(() => { console.log(calEvent); }, 250); }, []); const onDoubleClickEvent = useCallback((calEvent) => { console.log("onDoubleClick"); window.clearTimeout(clickRef?.current); clickRef.current = window.setTimeout(() => { console.log(calEvent); }, 250); }, []); const handleRangeChange = (range) => { const dateRange = Array.isArray(range) ? range : [range.start, range.end]; console.log("Date Range:", dateRange); // 更新当前显示日期 setCurrentDate(new Date(dateRange[0])); const newBackgroundEvents = getBackgroundEvents(dateRange); setBackgroundEvents(newBackgroundEvents); }; const getBackgroundEvents = (range) => { let events = []; let currentDate = new Date(range[0]); while (currentDate <= range[1]) { const day = (currentDate.getDay() + 6) % 7; const formattedDate = formatISO(currentDate, { representation: "date" }); const specialHours = specialOpeningHours.filter((special) => special.open.startsWith(formattedDate) ); let hours = specialHours.length > 0 ? specialHours : openingHours[day]; hours.forEach((slot) => { let start, end; if (specialHours.length > 0) { start = parseISO(slot.open); end = parseISO(slot.close); } else { start = parseISO(`${formattedDate}T${slot.open}`); end = parseISO(`${formattedDate}T${slot.close}`); } events.push({ start, end }); }); currentDate = new Date(currentDate); currentDate.setDate(currentDate.getDate() + 1); } return events; }; return ( <div className="w-full h-full"> <DnDCalendar defaultView="week" selectable="true" localizer={localizer} culture="de" events={myEventsList} startAccessor="start" endAccessor="end" style={{ height: "100%" }} messages={messages} views={[Views.WEEK, Views.DAY]} dayLayoutAlgorithm="no-overlap" onEventDrop={updateEvent} onEventResize={updateEvent} onSelectEvent={onSelectEvent} onSelectSlot={newEvent} backgroundEvents={backgroundEvents} onDoubleClick={onDoubleClickEvent} onRangeChange={handleRangeChange} date={currentDate} // 绑定受控日期 /> </div> ); } export default Scheduler;
关键说明
- 受控模式:通过
date属性手动控制日历显示的日期,避免重新渲染时重置范围 - 日期循环:每次创建新的Date对象,防止修改循环变量导致的日期比较错误
- 特殊时间覆盖逻辑:保持原有的特殊营业时间优先于常规时间的逻辑不变
内容的提问来源于stack exchange,提问作者blacklotus
相关产品推荐
相关产品推荐

