React-Big-Calendar周视图每次渲染自动滚动至顶部问题求助
问题
使用react-big-calendar的拖拽(DND)日历组件时,遇到以下问题:每当组件重新渲染(如状态变更或API调用完成后),周视图会自动滚动回到顶部。已尝试使用enableAutoScroll属性,但未能解决问题。
代码示例:
import React, { useState, useEffect, useMemo } from "react"; import { Calendar, momentLocalizer } from "react-big-calendar"; import "react-big-calendar/lib/css/react-big-calendar.css"; import moment from "moment"; import { Box, Text } from "@chakra-ui/react"; import CustomToolbar from "./CustomToolbar"; import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; import "react-big-calendar/lib/addons/dragAndDrop/styles.css"; import axios from "axios"; import firebase, { auth } from "../../firebase/firebase"; import { useAuthState } from "react-firebase-hooks/auth"; import { Droppable } from "react-beautiful-dnd"; import { fetchEvents, getAccessToken } from "../../helper/CalendarHelpers"; import "moment/locale/en-gb"; import { ContextMenu, ContextMenuItem, ContextMenuList, ContextMenuTrigger, } from "@saas-ui/react"; const server = process.env.REACT_APP_BACKEND_URL; const localizer = momentLocalizer(moment); const MyCalendar = ({ setEvents, events }) => { const DnDCalendar = withDragAndDrop(Calendar); const [user] = useAuthState(auth); const [date, setDate] = useState(moment()); const [startDay, setStartDay] = useState("Sunday"); const [loading, setLoading] = useState(false); const [scrollPosition, setScrollPosition] = useState(0); const [scrolltime, setscrolltime] = useState(new Date(0, 0, 0, 7)); useEffect(() => { fetchEvents(user, setEvents); }, []); const configureStartDay = (startDay) => { const dayIndex = ["su", "mo", "tu", "we", "th", "fr", "sa"].indexOf( startDay.slice(0, 2).toLowerCase() ); if (dayIndex === -1) { throw new Error("Invalid start day"); } moment.updateLocale("en", { week: { dow: dayIndex, }, }); return "en"; }; useEffect(() => { firebase .firestore() .collection("calendar") .doc(user.uid) .get() .then((snap) => { if (snap.exists) { if (snap?.data()?.startingDay) { setStartDay(snap?.data()?.startingDay); } } }) .catch((error) => { console.log("Error in Calendar.js===>", error); }); }, [user.uid]); const memoizedEvents = useMemo(() => { const CustomHeader = ({ date }) => { const formattedDate = moment(date).format("ddd DD"); return ( <Box style={{ userSelect: "none", display: "flex", flex: "1", flexDirection: "column", }} > <Text style={{ fontSize: "1rem", fontWeight: "normal", color: "#777272", }} > {formattedDate.split(" ")[0]} </Text> <Text style={{ fontSize: "1rem", fontWeight: "bold", }} > {formattedDate.split(" ")[1]} </Text> </Box> ); }; return CustomHeader; }, []); const handleDateSelect = (date) => { setDate(moment(date.toDate())); }; const handleNavigate = (navigate, view) => { if (navigate === "prev") { setDate(date.clone().subtract(1, view)); } else if (navigate === "next") { setDate(date.clone().add(1, view)); } }; const updateEvent = async (data) => { const { start, end, event, isAllDay } = data; if (isAllDay) { if (start.getDate() === end.getDate()) { start.setHours(0, 0, 0, 0); end.setDate(start.getDate() + 1); end.setHours(0, 0, 0, 0); } } const updatedEvent = { ...event, start, end }; let neweve = events.filter((x) => x.id !== updatedEvent.id); setEvents([...neweve, updatedEvent]); setscrolltime(start); try { const userId = user.uid; const calendarId = event.calendarId; let accessToken; let type; const calendarDoc = await firebase .firestore() .collection("calendar") .doc(userId) .get(); if (calendarId) { type = "refresh"; accessToken = getAccessToken( calendarId, calendarDoc.data().AllCalendar ); } await axios.put(`${server}/google/calendar/update-event/${event.id}`, { calendarId: calendarId, updatedEvent: updatedEvent, accessToken: accessToken, type: type, }); } catch (error) { console.error("Error updating event:", error); } }; const handleDelete = async (event) => { try { const userId = user.uid; const calendarId = event.calendarId; let accessToken; console.log("CALENDAR ID===>", calendarId); let type; const calendarDoc = await firebase .firestore() .collection("calendar") .doc(userId) .get(); if (calendarId) { type = "refresh"; accessToken = getAccessToken( calendarId, calendarDoc.data().AllCalendar ); } const filteredEvents = events.filter((e) => e.id !== event.id); setEvents(filteredEvents); await axios.delete(`${server}/google/calendar/delete-event/${event.id}`, { data: { calendarId: calendarId, accessToken: accessToken, }, }); } catch (error) { console.log("Error in Delete", error); } }; const onEventDrop = async (data) => { await updateEvent(data); }; const onEventResize = async (data) => { await updateEvent(data); }; const eventStyleGetter = () => { const style = { backgroundColor: "#EAF6FF", color: "#1C67D2", }; return { style }; }; const customTimeFormat = (date) => moment(date).format("h A"); const customEvents = ({ event, children }) => { return ( <ContextMenu> <ContextMenuTrigger longPressDisabled={true}> <div>{children}</div> </ContextMenuTrigger> <ContextMenuList pos="relative" bg="#33373d" minW={0} pt={0} pb={0}> <ContextMenuItem style={{ backgroundColor: "#33373d", color: "white", fontSize: 14, }} borderRadius="0.375rem" onClick={() => { handleDelete(event); }} > Delete </ContextMenuItem> </ContextMenuList> </ContextMenu> ); }; const CustomTimeSlot = ({ children, value }) => { return ( <Droppable droppableId={"Calendar-" + value}> {(provided) => ( <Box ref={provided.innerRef} {...provided.droppableProps} style={{ display: "flex", alignItems: "center" }} > {children} {provided.placeholder} </Box> )} </Droppable> ); }; const CustomWeek = (props) => { return <Box bg="red">{console.log(props)}</Box>; }; return ( <Box mx={6} style={{ height: "94vh", width: "100vw" }}> <style> {` .rbc-addons-dnd-row-body .rbc-row:nth-of-type(2) { display: none !important; } .rbc-event-allday { border: 2px solid #756ece !important; background-color : #dfe1f6 !important; } .rbc-event { border: 2px solid #756ece !important; background-color : #dfe1f6 !important; } .rbc-label{ font-size: 12px !important; color:#777272; } .rbc-time-header-content{ margin-left:45px; } .rbc-label rbc-time-header-gutter { display: none !important; } div.rbc-header.rbc-today p.chakra-text.css-0{ color: #3761EE !important; } div.rbc-header.rbc-today { border-bottom: 2px solid #3761EE; } div.rbc-header.rbc-today { background-color: white; } .rbc-day-slot .rbc-event-content { display: flex !important; justify-content: center !important; align-items: center !important; text-align: center !important; font-weight: 550 !important; color: #756ece !important;} .rbc-day-slot .rbc-event{ text-align:center; font-weight:550; border: 2px solid #756ece !important; background-color : #dfe1f6 !important; } .rbc-day-slot .rbc-events-container { margin-right:0px !important; } div.rbc-day-slot.rbc-time-column.rbc-now.rbc-today{ background-color:white; } .rbc-day-slot .rbc-event-label { display:none; } .rbc-timeslot-group { min-height:70px; } .rbc-header + .rbc-header { border-left: 1px solid #ddd; border-top: 1px solid #FFFFFF; border-bottom: 0px; } .rbc-time-view{ border:none; width: auto; } .rbc-time-content{ border-top:1px solid #ddd overflow-y:scroll; } .rbc-time-content::-webkit-scrollbar { display: none; } div.rbc-time-header.rbc-overflowing { margin-right: -2px !important; } .rbc-header{ border-bottom:0; text-align: start; padding-left: 20px; } .rbc-current-time-indicator { background-color: #3761EE !important; } .rbc-time-gutter .rbc-timeslot-group { display: block !important; width: 50px !important; } .rbc-time-header{ display:block !important } `} </style> <DnDCalendar key="12345" allDayAccessor="isAllDay" showAllEvents={true} showMultiDayTimes={true} localizer={localizer} events={events} startAccessor="start" endAccessor="end" defaultView="week" onEventDrop={onEventDrop} onEventResize={onEventResize} resizable style={{ height: "100%", width: "100%", }} components={{ toolbar: (props) => ( <CustomToolbar {...props} onNavigate={handleNavigate} date={date} onDateChange={handleDateSelect} /> ), header: memoizedEvents, eventWrapper: customEvents, timeSlotWrapper: CustomTimeSlot, week: CustomWeek, }} date={date.toDate()} eventPropGetter={eventStyleGetter} formats={{ timeGutterFormat: (date) => customTimeFormat(date), }} culture={configureStartDay(startDay)} /> </Box> ); }; export default MyCalendar;
解决方案
1. 避免在组件内部创建拖拽日历实例
你当前在MyCalendar组件内部执行const DnDCalendar = withDragAndDrop(Calendar);,这会导致每次组件渲染时都重新创建一个新的日历组件类,触发组件的卸载和重新挂载,直接丢失滚动位置。
修复方式:将拖拽日历的创建移到组件外部:
// 移到组件外部,只执行一次 const DnDCalendar = withDragAndDrop(Calendar); const MyCalendar = ({ setEvents, events }) => { // ... 剩余代码 };
2. 保存并恢复滚动位置
利用useRef获取日历的滚动容器,在组件更新前记录滚动位置,更新后恢复:
步骤:
- 添加ref引用滚动容器
- 监听
events变化(或其他触发重渲染的状态),在更新前保存位置,更新后恢复
const MyCalendar = ({ setEvents, events }) => { const timeContentRef = useRef(null); const scrollPosRef = useRef(0); // 保存滚动位置(在组件更新前) useEffect(() => { if (timeContentRef.current) { scrollPosRef.current = timeContentRef.current.scrollTop; } }, [events]); // 监听导致重渲染的状态,比如events // 恢复滚动位置(组件更新后) useEffect(() => { if (timeContentRef.current) { timeContentRef.current.scrollTop = scrollPosRef.current; } }, [events]); // ... 剩余代码 // 在return中给滚动容器添加ref return ( <Box mx={6} style={{ height: "94vh", width: "100vw" }}> <style>{/* ... 样式代码 */}</style> {/* 通过containerRef传递ref到日历内部的滚动容器 */} <DnDCalendar // ... 其他props containerRef={timeContentRef} /> </Box> ); };
3. 优化自定义组件的重渲染
你的自定义组件(如customEvents、CustomTimeSlot)每次渲染都会重新创建,导致日历的子组件频繁重渲染,间接影响滚动。用React.memo包裹这些组件:
import React, { useState, useEffect, useMemo, memo } from "react"; // 用memo包裹自定义事件组件 const customEvents = memo(({ event, children }) => { return ( <ContextMenu> <ContextMenuTrigger longPressDisabled={true}> <div>{children}</div> </ContextMenuTrigger> <ContextMenuList pos="relative" bg="#33373d" minW={0} pt={0} pb={0}> <ContextMenuItem style={{ backgroundColor: "#33373d", color: "white", fontSize: 14, }} borderRadius="0.375rem" onClick={() => { handleDelete(event); }} > Delete </ContextMenuItem> </ContextMenuList> </ContextMenu> ); }); // 用memo包裹自定义时间槽组件 const CustomTimeSlot = memo(({ children, value }) => { return ( <Droppable droppableId={"Calendar-" + value}> {(provided) => ( <Box ref={provided.innerRef} {...provided.droppableProps} style={{ display: "flex", alignItems: "center" }} > {children} {provided.placeholder} </Box> )} </Droppable> ); });
4. 移除不必要的固定key
当前DnDCalendar上设置了固定key="12345",虽然不会直接导致滚动问题,但如果后续需要基于状态变化更新key,固定值会失去
相关产品推荐
相关产品推荐

