React中AddEventListener返回历史事件而非当前事件的问题求助
问题:Kendo React Grid到Scheduler拖拽时Drop事件重复触发历史事件,dragItem有时为undefined
我是React和Kendo React新手,按照官方教程实现Grid到Scheduler的拖拽功能,但处理Scheduler的Drop事件时,监听器会返回所有历史事件,有时还会出现undefined的dragItem。试过清空dragItem状态、移除监听器,都没解决。
附上相关代码:
import * as React from "react"; import { guid } from "@progress/kendo-react-common"; import { timezoneNames } from "@progress/kendo-date-math"; import { Scheduler, TimelineView, DayView, WeekView, MonthView, AgendaView, } from "@progress/kendo-react-scheduler"; import "@progress/kendo-date-math/tz/Etc/UTC"; import "@progress/kendo-date-math/tz/Europe/Sofia"; import "@progress/kendo-date-math/tz/Europe/Madrid"; import "@progress/kendo-date-math/tz/Asia/Dubai"; import "@progress/kendo-date-math/tz/Asia/Tokyo"; import "@progress/kendo-date-math/tz/America/New_York"; import "@progress/kendo-date-math/tz/America/Los_Angeles"; import { Grid, GridColumn } from "@progress/kendo-react-grid"; import gridData from "../StaticData/data.js"; import { sampleDataWithCustomSchema, displayDate, customModelFields, } from "../StaticData/events-utc"; const Schedule = () => { const timezones = React.useMemo(() => timezoneNames(), []); const locales = [ { language: "en-US", locale: "en", }, { language: "es-ES", locale: "es", }, ]; const [view, setView] = React.useState("day"); const [date, setDate] = React.useState(displayDate); const [locale, setLocale] = React.useState(locales[0]); const [timezone, setTimezone] = React.useState("Etc/UTC"); const [orientation, setOrientation] = React.useState("horizontal"); const [data, setData] = React.useState(sampleDataWithCustomSchema); const [Griddata, setGridData] = React.useState(gridData); const handleViewChange = React.useCallback( (event) => { setView(event.value); }, [setView] ); const handleDateChange = React.useCallback( (event) => { setDate(event.value); }, [setDate] ); const handleLocaleChange = React.useCallback( (event) => { setLocale(event.target.value); }, [setLocale] ); const handleTimezoneChange = React.useCallback( (event) => { setTimezone(event.target.value); }, [setTimezone] ); const handleDataChange = React.useCallback( ({ created, updated, deleted }) => { setData((old) => old .filter( (item) => deleted.find((current) => current.TaskID === item.TaskID) === undefined ) .map( (item) => updated.find((current) => current.TaskID === item.TaskID) || item ) .concat( created.map((item) => Object.assign({}, item, { TaskID: guid(), }) ) ) ); }, [setData] ); function Exists(taskId) { return data.some(function(el) { return el.TaskID === taskId; }); } const handleDropItem = (e) => { console.log(dragItem) // if(Exists(dragItem.taskID)==false ){ // let start = e.target.getAttribute("data-slot-start"); // let end = e.target.getAttribute("data-slot-end"); // let room = e.target.getAttribute("data-slot-group"); // let startDate = new Date(parseInt(start)); // let endDate = new Date(parseInt(end)); // let newEvent = { // TaskID: dragItem.taskID, // End: endDate, // PersonIDs: 1, // RoomID: 2, // Start: startDate, // Title: dragItem.title , // isAllDay: false // } // setData(oldData=>[newEvent, ...oldData]) // setGridData(Griddata.filter(item=>item.taskID!== dragItem.taskID)) // } }; React.useEffect(() => { let schedulerElement = MyScheduler.current.element; schedulerElement.addEventListener("drop", handleDropItem); schedulerElement.addEventListener("dragover", (e) => e.preventDefault()); }); React.useEffect(() => { }, [data]); const MyScheduler = React.createRef(); const [dragItem, setDragItem] = React.useState(""); const GridRowRender = (tr, props) => { const trProps = { draggable: true, onDragStart: (e) => { console.log(e) setDragItem(props.dataItem); } }; return React.cloneElement(tr, { ...trProps }, tr.props.children); }; return ( <div> <Scheduler data={data} onDataChange={handleDataChange} view={view} onViewChange={handleViewChange} date={date} onDateChange={handleDateChange} editable={true} timezone={timezone} modelFields={customModelFields} group={{ resources: ["Rooms", "Persons"], orientation, }} resources={[ { name: "Rooms", data: [ { text: "Meeting Room 101", value: 1, }, { text: "Meeting Room 201", value: 2, color: "#FF7272", }, ], field: "RoomID", valueField: "value", textField: "text", colorField: "color", }, { name: "Persons", data: [ { text: "Peter", value: 1, color: "#5392E4" }, { text: "Alex", value: 2, color: "#54677B", }, ], multiple:false, field: "PersonIDs", valueField: "value", textField: "text", colorField: "color" }, ]} ref={MyScheduler} > <TimelineView /> <DayView /> <WeekView /> <MonthView /> <AgendaView /> </Scheduler> <hr /> <Grid data={Griddata} rowRender={GridRowRender}> <GridColumn field="taskID" /> <GridColumn field="title" /> </Grid> </div> ); }; export default Schedule
我尝试过清空dragItem的状态、清除监听器,但均无效果。
解决方案
问题根源
- 监听器重复绑定:
useEffect未设置依赖数组,组件每次渲染都会给Scheduler元素添加新的drop和dragover监听器,多次拖拽后监听器堆积,导致每次drop触发所有历史监听器,出现重复事件。 - 闭包捕获旧状态:
handleDropItem函数捕获的是组件渲染时的dragItem旧值,状态更新后旧监听器仍持有旧引用,会出现undefined或历史值。 - 未清理监听器:组件卸载时未移除绑定的监听器,引发内存泄漏和不必要的事件触发。
- 拖拽后未重置状态:拖拽结束后未清空
dragItem,下次拖拽前可能残留旧值。
修改后的代码要点
const Schedule = () => { // ... 保留原有状态和函数 // 用useRef保存实时拖拽项,规避闭包问题 const dragItemRef = React.useRef(null); const [dragItem, setDragItem] = React.useState(null); // 状态更新时同步更新ref React.useEffect(() => { dragItemRef.current = dragItem; }, [dragItem]); const handleDropItem = (e) => { const currentDragItem = dragItemRef.current; if (!currentDragItem) return; // 无有效拖拽项时直接返回 console.log(currentDragItem); if (!Exists(currentDragItem.taskID)) { let start = e.target.getAttribute("data-slot-start"); let end = e.target.getAttribute("data-slot-end"); let room = e.target.getAttribute("data-slot-group"); let startDate = new Date(parseInt(start)); let endDate = new Date(parseInt(end)); let newEvent = { TaskID: currentDragItem.taskID, End: endDate, PersonIDs: 1, RoomID: 2, Start: startDate, Title: currentDragItem.title, isAllDay: false }; setData(oldData => [newEvent, ...oldData]); setGridData(prev => prev.filter(item => item.taskID !== currentDragItem.taskID)); } // 拖拽完成后清空状态 setDragItem(null); }; React.useEffect(() => { const schedulerElement = MyScheduler.current.element; // 绑定监听器 schedulerElement.addEventListener("drop", handleDropItem); const dragoverHandler = (e) => e.preventDefault(); schedulerElement.addEventListener("dragover", dragoverHandler); // 返回清理函数,组件卸载或effect重新执行时移除监听器 return () => { schedulerElement.removeEventListener("drop", handleDropItem); schedulerElement.removeEventListener("dragover", dragoverHandler); }; }, []); // 空依赖数组,只执行一次绑定 const GridRowRender = (tr, props) => { const trProps = { draggable: true, onDragStart: (e) => { setDragItem(props.dataItem); // 可选:设置拖拽数据,增强兼容性 e.dataTransfer.setData("text/plain", JSON.stringify(props.dataItem)); }, onDragEnd: () => { // 拖拽结束无论成功失败,清空状态 setDragItem(null); } }; return React.cloneElement(tr, { ...trProps }, tr.props.children); }; // ... 保留原有返回JSX部分 };
关键修改说明
- useRef保存实时拖拽项:
dragItemRef同步dragItem的最新值,确保handleDropItem总能拿到当前最新的拖拽数据,避免闭包问题。 - 清理监听器:在
useEffect的返回函数中移除绑定的监听器,防止重复绑定和内存泄漏,空依赖数组保证只绑定一次。 - 拖拽结束重置状态:在
onDragEnd和handleDropItem末尾清空dragItem,避免旧值残留导致的undefined或重复事件。 - 增加空值判断:在
handleDropItem开头检查拖拽项是否存在,直接过滤无效的drop事件。
内容的提问来源于stack exchange,提问作者ScottNoClue
相关产品推荐
相关产品推荐

