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

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的状态、清除监听器,但均无效果。


解决方案

问题根源

  1. 监听器重复绑定:useEffect未设置依赖数组,组件每次渲染都会给Scheduler元素添加新的drop和dragover监听器,多次拖拽后监听器堆积,导致每次drop触发所有历史监听器,出现重复事件。
  2. 闭包捕获旧状态:handleDropItem函数捕获的是组件渲染时的dragItem旧值,状态更新后旧监听器仍持有旧引用,会出现undefined或历史值。
  3. 未清理监听器:组件卸载时未移除绑定的监听器,引发内存泄漏和不必要的事件触发。
  4. 拖拽后未重置状态:拖拽结束后未清空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部分
};

关键修改说明

  1. useRef保存实时拖拽项:dragItemRef同步dragItem的最新值,确保handleDropItem总能拿到当前最新的拖拽数据,避免闭包问题。
  2. 清理监听器:在useEffect的返回函数中移除绑定的监听器,防止重复绑定和内存泄漏,空依赖数组保证只绑定一次。
  3. 拖拽结束重置状态:在onDragEnd和handleDropItem末尾清空dragItem,避免旧值残留导致的undefined或重复事件。
  4. 增加空值判断:在handleDropItem开头检查拖拽项是否存在,直接过滤无效的drop事件。

内容的提问来源于stack exchange,提问作者ScottNoClue

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 19:10:32