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

React+TypeScript中FullCalendar拖拽多事件及列表更新问题求助

问题:拖拽FullCalendar生成多个事件且事件列表不更新

在React+TypeScript项目中集成FullCalendar和Draggable组件时,拖拽「Bölgeler」容器内的块到日历,会生成多个重复事件,同时状态中的事件列表无法正确同步到日历。

原代码

import FullCalendar from "@fullcalendar/react";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin, {
  Draggable,
  DropArg,
} from "@fullcalendar/interaction";
import allLocales from "@fullcalendar/core/locales-all";
import React, { useEffect, useState } from "react";

interface MyEvent {
  id: string;
  title: string;
  date: string;
  color: string;
}

function App() {
  const [events, setEvents] = useState<MyEvent[]>([]);

  useEffect(() => {
    const containerEl = document.querySelector("#bolgeler");

    if (containerEl instanceof HTMLElement) {
      new Draggable(containerEl, {
        itemSelector: ".bolge",
        eventData: (eventEl) => {
          return {
            id: Math.floor(Math.random() * 1000).toString(),
            title: eventEl.innerText,
            color: window.getComputedStyle(eventEl).backgroundColor,
          };
        },
      });
      console.log(events);
    } else {
      console.error("Container element not found");
    }
  }, []);

  const handleEventDrop = (dropInfo: DropArg) => {
    const { draggedEl, date } = dropInfo;
    const title = draggedEl.innerText;
    const color = window.getComputedStyle(draggedEl).backgroundColor;

    const newEvent = {
      id: Math.floor(Math.random() * 1000).toString(),
      title,
      date: date.toISOString(),
      color,
    };

    setEvents((prevEvents) => [...prevEvents, newEvent]);

    console.log(events);
  };

  const handleEventRemove = (eventToRemoveId: string) => {
    setEvents((prevEvents) =>
      prevEvents.filter((event) => event.id !== eventToRemoveId)
    );
  };

  return (
    <>
      <div id="takvim">
        <FullCalendar
          windowResizeDelay={100}
          handleWindowResize={true}
          height={650}
          plugins={[timeGridPlugin, interactionPlugin]}
          slotLabelFormat={{
            hour: "2-digit",
            minute: "2-digit",
          }}
          editable={true}
          droppable={true}
          allDaySlot={false}
          dayHeaderFormat={{
            weekday: "long",
          }}
          locales={allLocales}
          locale="tr"
          buttonText={{
            today: "Bugün",
          }}
          events={{ events }}
          drop={() => handleEventDrop}
          eventClick={(info) => {
            if (
              window.confirm(
                `Are you sure you want to delete the event '${info.event.id}'?`
              )
            ) {
              handleEventRemove(info.event.id);
            }
          }}
        />
      </div>

      <div id="bolgeler">
        <p>
          <strong>Bölgeler</strong>
        </p>
        <div className="bolge" style={{ backgroundColor: "red" }}>
          Bölge 1
        </div>
        <div className="bolge" style={{ backgroundColor: "green" }}>
          Bölge 2
        </div>
        <div className="bolge" style={{ backgroundColor: "blue" }}>
          Bölge 3
        </div>
        <div className="bolge" style={{ backgroundColor: "orange" }}>
          Bölge 4
        </div>
        <div className="bolge" style={{ backgroundColor: "brown" }}>
          Bölge 5
        </div>
      </div>
    </>
  );
}
export default App;

核心问题分析

  1. 重复事件原因:同时开启了Draggable的eventData自动生成事件,以及手动在drop事件中添加事件,导致两次创建事件。
  2. 事件列表不更新原因:FullCalendar的events属性错误传递为{{ events }}(对象嵌套),而非直接传递事件数组events;同时drop事件绑定错误,写成了() => handleEventDrop,导致回调未正确触发。
  3. 日志无效原因:setState是异步操作,直接打印events无法获取最新状态。

修复方案

1. 修正FullCalendar属性绑定

  • 将drop={() => handleEventDrop}改为drop={handleEventDrop},确保事件回调正确触发。
  • 将events={{ events }}改为events={events},让日历直接读取状态中的事件数组。

2. 移除Draggable自动生成事件的配置

删除Draggable中的eventData选项,避免自动添加事件,仅保留拖拽交互能力:

new Draggable(containerEl, {
  itemSelector: ".bolge",
  // 移除eventData配置
});

3. 修复状态日志问题

通过useEffect监听events变化,打印最新状态:

useEffect(() => {
  console.log("当前事件列表:", events);
}, [events]);

4. 优化DOM元素获取(可选)

用React的useRef代替document.querySelector,更符合组件化规范:

const bolgelerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (bolgelerRef.current) {
    new Draggable(bolgelerRef.current, {
      itemSelector: ".bolge",
    });
  }
}, []);

// 绑定到容器元素
<div id="bolgeler" ref={bolgelerRef}>

5. 确保事件ID唯一性(可选)

替换随机ID生成逻辑,避免重复:

id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,

修复后完整代码

import FullCalendar from "@fullcalendar/react";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin, {
  Draggable,
  DropArg,
} from "@fullcalendar/interaction";
import allLocales from "@fullcalendar/core/locales-all";
import React, { useEffect, useState, useRef } from "react";

interface MyEvent {
  id: string;
  title: string;
  date: string;
  color: string;
}

function App() {
  const [events, setEvents] = useState<MyEvent[]>([]);
  const bolgelerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (bolgelerRef.current) {
      new Draggable(bolgelerRef.current, {
        itemSelector: ".bolge",
      });
    }
  }, []);

  useEffect(() => {
    console.log("当前事件列表:", events);
  }, [events]);

  const handleEventDrop = (dropInfo: DropArg) => {
    const { draggedEl, date } = dropInfo;
    const title = draggedEl.innerText;
    const color = window.getComputedStyle(draggedEl).backgroundColor;

    const newEvent = {
      id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
      title,
      date: date.toISOString(),
      color,
    };

    setEvents((prevEvents) => [...prevEvents, newEvent]);
  };

  const handleEventRemove = (eventToRemoveId: string) => {
    setEvents((prevEvents) =>
      prevEvents.filter((event) => event.id !== eventToRemoveId)
    );
  };

  return (
    <>
      <div id="takvim">
        <FullCalendar
          windowResizeDelay={100}
          handleWindowResize={true}
          height={650}
          plugins={[timeGridPlugin, interactionPlugin]}
          slotLabelFormat={{
            hour: "2-digit",
            minute: "2-digit",
          }}
          editable={true}
          droppable={true}
          allDaySlot={false}
          dayHeaderFormat={{
            weekday: "long",
          }}
          locales={allLocales}
          locale="tr"
          buttonText={{
            today: "Bugün",
          }}
          events={events}
          drop={handleEventDrop}
          eventClick={(info) => {
            if (
              window.confirm(
                `Are you sure you want to delete the event '${info.event.title}'?`
              )
            ) {
              handleEventRemove(info.event.id);
            }
          }}
        />
      </div>

      <div id="bolgeler" ref={bolgelerRef}>
        <p>
          <strong>Bölgeler</strong>
        </p>
        <div className="bolge" style={{ backgroundColor: "red" }}>
          Bölge 1
        </div>
        <div className="bolge" style={{ backgroundColor: "green" }}>
          Bölge 2
        </div>
        <div className="bolge" style={{ backgroundColor: "blue" }}>
          Bölge 3
        </div>
        <div className="bolge" style={{ backgroundColor: "orange" }}>
          Bölge 4
        </div>
        <div className="bolge" style={{ backgroundColor: "brown" }}>
          Bölge 5
        </div>
      </div>
    </>
  );
}
export default App;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 11:29:50