fullcalendar / fullcalendar-react

The official React Component for FullCalendar
https://fullcalendar.io/docs/react
MIT License
2.09k stars 110 forks source link

Dragging external events generates duplicates on calendar #118

Closed mauretto78 closed 3 years ago

mauretto78 commented 3 years ago

IMPORTANT PREMISE:

I don't understand if is a duplicate of this, in case delete the issue:

https://github.com/fullcalendar/fullcalendar/issues/7067

DESCRIPTION OF BUG

When dragging an external event to the calendar, one or more duplicates of the event are generated.

STEP TO REPRODUCE (WITH SAMPLE CODE)

I have a root component called PageCalendar. This component contains a list of draggable events and the calendar.

This is my event list:

// EventList.js

const EventList = (props) => {

    useEffect(() => {
        let draggableEl = document.getElementById("external-events");
        new Draggable(draggableEl, {
            itemSelector: ".fc-event",
            eventData: function(eventEl) {
                let id = eventEl.dataset.id;
                let title = eventEl.getAttribute("title");
                let color = eventEl.dataset.color;
                let custom = eventEl.dataset.custom;

                return {
                    id: id,
                    title: title,
                    color: color,
                    custom: custom,
                    create: true
                };
            }
        });
    });

  // other stuff

And this is the calendar (notice that I passed events and event handling functions from the root component via props):

// Calendar.js

const Calendar = (props) => {

    // other stuff

    return (
        <>
            <FullCalendar
                locale={itLocale}
                plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
                headerToolbar={{
                    left: 'prev,next today',
                    center: 'title',
                    right: 'dayGridMonth,timeGridWeek,timeGridDay'
                }}
                initialView='dayGridMonth'
                editable={true}
                selectable={true}
                selectMirror={true}
                dayMaxEvents={true}
                weekends={props.weekendsVisible}
                events={props.events}
                droppable={true}
                eventContent={renderEventContent} 
                eventClick={handleEventClick}
                eventReceive={props.handleEventReceive}
            />
        </>
    );
};

export default Calendar;

And finally, this is my root component:

// PageCalendar.js

const PageCalendar = () => {

    // initial state
    const [state, setState] = useState({
        weekendsVisible: true,
        calendarEvents: getEvents(),
        externalEvents: externalEvents(),
        filterEvents: {
            cat: null,
            id: null
        }
    });

   const handleEventReceive = (eventInfo) => {
        const newEvent = {
            id: eventInfo.draggedEl.getAttribute("data-id"),
            title: eventInfo.draggedEl.getAttribute("title"),
            color: eventInfo.draggedEl.getAttribute("data-color"),
            start: eventInfo.date,
            end: eventInfo.date,
            custom: eventInfo.draggedEl.getAttribute("data-custom")
        };

        setState(state => {
            return {
                ...state,
                calendarEvents:state.calendarEvents.concat(newEvent)
            };
        });
    };

 return (
        <>
            <Header
                addEvent={handleAddExternalEvent}
                weekendsVisible={state.weekendsVisible}
                filterEvents={handleFilterCalendarEvents}
                weekendsToggle={handleWeekendsToggle}
            />
            <CCard>
                <CCardBody>
                    <CRow>
                        <CCol md="2">
                            <EventList
                                deleteEvent={handleDeleteEvent}
                                externalEvents={state.externalEvents}
                                filterEvents={handleFilterEvents}
                            />
                        </CCol>
                        <CCol md="10">
                            <Calendar
                                events={state.calendarEvents}
                                handleEventReceive={handleEventReceive}
                                weekendsVisible={state.weekendsVisible}
                            />
                        </CCol>
                    </CRow>
                </CCardBody>
            </CCard>
        </>
    );
};

export default PageCalendar;

Now, when you drag elements:

I already tried with created: false in EventList and using drop instead of eventReceive in Calendar, but the result is the same.

I am sorry for bothering you, and I hope this can help someone.

Many thanks in advance :)

acerix commented 3 years ago

Would you be able to post a runnable demonstration of the bug? Would really appreciate it because the time saved reproducing will be time spent fixing.

mauretto78 commented 3 years ago

@acerix good morning, sorry for my late reply.

I reproduced here the bug:

https://codesandbox.io/s/white-dawn-knv4s?file=/src/App.js

As you can see, after dragging an element to the calendar, duplicates are generated.

I figured out that could be an issue related to bad state management.

I am so sorry for bothering you, I hope if you need more details don't hesitate to ask.

Thank you in advance

M,

acerix commented 3 years ago

Thanks for the demo, it does seem like a bug, liekly related to the issue you mentioned but I'm not sure it's a duplicate.

arshaw commented 3 years ago

The problem is that your useEffect callback is being called whenever there is any state change to your root component, subsequently reinitializing the Draggable without destroying it. These duplicate initializations on the same element result in multiple events being added each time.

The solution is to make sure new Draggable is only called once per external element. Here is a solution that involves moving useEffect within a subcomponent, useRef, and memo (to prevent unnecessary rerendering).

https://codesandbox.io/s/fullcalendar-react-draggable-forked-ehr0h?file=/src/App.js

mauretto78 commented 3 years ago

@arshaw Sorry for my late reply.

Many thanks. it worked like a charm!!

anttituomola commented 2 years ago

Hi all, and thanks a lot for the conversation so far. I've wrestled with this issue for two days now with no luck, so I have to ask for your help.

In the Codesandbox example from @arshaw start and end dates return as undefined when running handleEventReceive function. Therefore they will get saved to the state but do not get rendered to the calendar, as they do not have a valid start time.

When I changed the newEvent variable to grab a valid date (start: eventInfo.event.startStr), the event gets rendered to the calendar. However, it renders event as double, as it fetches one from the state and the one that get's dragged there.

The dragged event will get rendered to the calendar without the events source or even without the eventReceive handler. Is there a way to prevent this and only render the events from the state (or from another source? Or is there another way to prevent double rendering?

Here's a live demo and here's the code

I'm sure I've misunderstood this somehow, so all help is much appreciated!

anttituomola commented 2 years ago

Oh, and I noticed that more useful example would have the code and the live demo combined, so here's the StackBlitz of my example. @mauretto78 how did you solve this issue?

ameotoko commented 2 years ago

I just want to leave a note here, because someone might still stumble upon this problem (as I did). The solution might be more simple than in @arshaw's comment.

The problem is that your useEffect callback is being called whenever there is any state change to your root component, subsequently reinitializing the Draggable without destroying it

Passing an empty dependency array as a second argument to useEffect should be already enough to make sure it's called only once. But the thing is, React intentionally invokes all the hooks twice in Strict Mode (i.e. in development). This is described in the docs. If you return a cleanup function from useEffect, the result of the duplicate call will be immediately cleaned up, and no duplicate events will be created.

TLDR:

  1. pass empty array as a second argument to useEffect to prevent re-initialization when the component is re-rendered;
  2. return a cleanup function from useEffect callback, which destroys the Draggable instance:
// load external events
  useEffect(() => {
    let draggableEl = document.getElementById("external-events");
-   new Draggable(draggableEl, {
+   const draggable = new Draggable(draggableEl, {
      itemSelector: ".fc-event",
      eventData: function (eventEl) {
        ...
      }
    });

+   return () => draggable.destroy();
- });
+ }, []);
josiah-brown commented 1 year ago

@ameotoko that solution worked for me. Thanks!

TechCodeDev commented 1 year ago

same issue we are facing in javascript when we call method calender again in a different senerio please any one help us

 function Calender()
    {

        var Draggable = FullCalendar.Draggable;
        var containerEl = document.getElementById('accordion');
        new Draggable(containerEl, {
            itemSelector: '.fc-event',
            eventData: function (eventEl)
            {

                return {
                    title: eventEl.innerText
                };
            },
            zIndex: 999,
            revert: true,
            revertDuration: 0
        });
        var calendarEl = document.getElementById('Calender');
        calendar = new FullCalendar.Calendar(calendarEl,
            {
                timeZone: 'UTC',
                minTime: "9am",
                maxTime: "9pm",
                headerToolbar: {
                    left: 'prev,next',
                    center: 'title',
                    right: ''
                },
                aspectRatio: 1.5,
                editable: false,
                height: 580,
                initialView: 'timeGridWeek',
                resources: ResourcesArr,
                events: EventsArr,
                schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
                droppable: true,
                drop: function (dropInfo) {

                },
                eventReceive: function (info)
                {

                },
                eventClick: function (calEvent, jsEvent, view) {

                },

            });
        calendar.render();

    }
darkengine commented 1 year ago

I use JavaScript style to create the draggable in an useEffect callback:

new Draggable(containerEl, {
  itemSelector: '.item-class',
  eventData: function(eventEl) {
    return {
      title: eventEl.innerText,
      duration: '02:00'
    };
  }
});

And it proves me wrong since, thanks for the answers above. I realize the draggable need to be created per event:

  useEffect(() => {
    let draggable = new Draggable(elRef.current, {
      eventData: function () {
        return { ...event, create: true };
      }
    });

    // a cleanup function
    return () => draggable.destroy();
  });

Otherwise, multiple draggable will be created for one external event. And when dragging this external event, it will generate multiple events on the fullcalendar.