Error on EventCalendar when inside a tab in React

when EventCalendar is inside a tab, error when selecting a different tab, onUnmount.
Using react-bootstrap tabs, and functional react with typescript.

mobiscroll.react.min.js:2 Uncaught TypeError: Cannot read properties of undefined (reading 'unsubscribe')
    at t._destroy (mobiscroll.react.min.js:2:1)
    at t.componentWillUnmount (mobiscroll.react.min.js:2:1)
    at eval (eval at callComponentWillUnmountWithTimer (vendors-02d1408ccd4d994dcf08.js:1:1), <anonymous>:1:10)
    at callComponentWillUnmountWithTimer (react-dom.development.js:19577:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237:1)
    at invokeGuardedCallback (react-dom.development.js:292:1)
    at safelyCallComponentWillUnmount (react-dom.development.js:19587:1)
    at commitUnmount (react-dom.development.js:20109:1)
    at commitNestedUnmounts (react-dom.development.js:20163:1)

Hello Kate,

Could you please share your relevant code so we can reproduce this?

Yep. I’ve pinpointed the issue to 1. when toggling between tabs, the issue is with the api call/promise. It does not error out when using your getJson example. 2. The same error occurs when you try to toggle the calendar view with getJson as well as the api call.

const App = () => (
  <Tabs unmountOnExit id="library-tabs" defaultActiveKey={"other"}>
    <Tab key="other" eventKey="other" title="Other">
      <div>Other Stuff</div>
    </Tab>
    <Tab key="calendar" eventKey="calendar" title="Calendar">
      <TasksCalendar />
    </Tab>
  </Tabs>
);

const TasksCalendar = () => {
  const [events, setEvents] = useState<Array<MbscCalendarEvent>>([]);

  const onPageLoading = (event, inst) => {
    service.getTimeline({
      caseId,
      params: {
        start_date: moment(event.firstDay).format("YYYY-MM-DD"),
        end_date: moment(event.lastDay).format("YYYY-MM-DD"),
      },
    }).then((events) => {
      setEvents(events);
    });
  };

  return (
    <div>
      <EventCalendar
        data={events}
        onPageLoading={onPageLoading}
        popUpTitle1={gettext("Status")}
        popUpTitle2={gettext("Due On")}
      />
    </div>
  );
};

export default TasksCalendar;


const EventCalendar = (props) => {
  const { width, height } = useWindowSize();
  const [view, setView] = useState("month");
  const [largeView, setLargeView] = useState(false);
  const [isOpen, setOpen] = useState(false);
  const [event, setEvent] = useState<MbscCalendarEvent | undefined>();
  const [anchor, setAnchor] = React.useState();

  const [calView, setCalView] = useState<MbscEventcalendarView>({
    calendar: {
      labels: true,
      type: "month",
    },
  });

  setOptions({
    theme: "ios",
    themeVariant: "light",
  });


  useEffect(() => {
    setLargeView(width > 600);

    if (width <= 600) {
      setCalView({
        calendar: { type: "month" },
        agenda: { type: "month" },
      });
    }
  }, [width]);

  const onEventClick = (e) => {
    setOpen(e.event.id !== event?.id ? true : !isOpen);
    setAnchor(e.domEvent.target);
    setEvent(e.event);
  };

  const changeView = (e) => {
    let labels = largeView;
    let view = {};
    switch (e.target.value) {
      case "month":
        view = {
          calendar: { labels, type: "month" },
        };
        if (!labels) {
          view["agenda"] = { type: "month" };
        }
        break;
      case "week":
        view = {
          calendar: { labels, type: "week" },
          agenda: { type: "week" },
        };
        break;
      case "day":
        view = {
          agenda: { type: "day" },
        };
        break;
    }
    setView(e.target.value);
    setCalView(view);
  };

  const customWithNavButtons = () => {
    return (
      <>
        <CalendarNav className="cal-header-nav mr-auto" />
        <CalendarPrev className="cal-header-prev align-self-center" />
        {largeView && <CalendarToday className="md-custom-header-today" />}
        <CalendarNext className="cal-header-next align-self-center" />
        <div className="cal-header-picker ml-auto">
          <SegmentedGroup themeVariant="light" value={view} onChange={changeView}>
            <SegmentedItem value="month">{largeView ? "Month" : "M"}</SegmentedItem>
            <SegmentedItem value="week">{largeView ? "Week" : "W"}</SegmentedItem>
            <SegmentedItem value="day">{largeView ? "Day" : "D"}</SegmentedItem>
          </SegmentedGroup>
        </div>
      </>
    );
  };

  const renderLabel = (data) => {
    if (data.isMultiDay) {
      return (
        <div style={{ color: "#000" }} className={`${data.original.className} pl-1 multi-day-event fs-16`}>
          {" "}
          {data.original.title}
        </div>
      );
    } else {
      return (
        <>
          <div className="single-day-event-dot" style={{ background: data.original.color }}></div>
          <div style={{ color: "#000" }} className={`${data.original.className} pl-1 single-day-event fs-16`}>
            {data.original.title}
          </div>
        </>
      );
    }
  };

  return (
    <div className="md-switching-view-cont">
      <Eventcalendar
        theme="ios"
        themeVariant="light"
        renderHeader={customWithNavButtons}
        view={calView}
        cssClass="md-custom-header"
        renderLabel={renderLabel}
        onEventClick={onEventClick}
        {...props}
      />
      <Popup
        display="anchored"
        isOpen={isOpen}
        anchor={anchor}
        touchUi={false}
        showOverlay={false}
        contentPadding={false}
        closeOnOverlayClick={true}
        closeOnEsc={true}
        width={350}
        onClose={() => setOpen(false)}
        cssClass="md-tooltip"
      >
        <div>
          <div className={`px-2 md-tooltip-header py-1 ${event?.className}`}>
            <span className="md-tooltip-title fs-20 font-weight-semi-bold">{event?.title}</span>
          </div>
          <div className="pt-1 px-2 md-tooltip-info mb-2">
            <div className="md-tooltip-title fs-14 pb-1">
              <div>{props.popUpTitle1 || ""}</div>
              <div className="pt-1">{event?.statusBadge}</div>
            </div>
            <hr />
            <div className="md-tooltip-title fs-14 pt-1">
              <div>{props.popUpTitle2 || ""}</div>
              <div className="pt-1 pb-2">
                {event?.end && moment.isMoment(event.end) ? event.end.format("MM-DD-YYYY") : null}
              </div>
            </div>
            {event?.button}
          </div>
        </div>
      </Popup>
    </div>
  );
};

export default EventCalendar;

We are seing this too, I think it is related to react strict mode (and now always react 18). It is calling willUnmount twice and in the second run something is not there anymore

It is here: /core/components/eventcalendar/scheduler/schedule-event.ts in this code:

protected _destroy() {
    if (this._el) {
      this._el.blur();
    }
    if (this._unsubscribe) {
      const id = this.s.event.uid!;
      const observable = stateObservables[id];
        observable.unsubscribe(this._unsubscribe);
        if (!observable.nr) {
          delete stateObservables[id];
        }
    }

    if (this._unlisten) {
      this._unlisten();
    }

    unlisten(this._doc, TOUCH_START, this._onDocTouch);
    unlisten(this._doc, MOUSE_DOWN, this._onDocTouch);
  }

Wrapping observable.unsubscr… in an if (observable) { solves it

Hi folks!

Unfortunately I still could not reproduce this with Mobiscroll Version 5.15.2 and react 18. Can any of you put together a simple project where this can be reproduced?

Thanks,
Zoli

Make sure you are using the new createRoot function:

const root = createRoot(document.getElementById(‘mainMountPoint’));
root.render();

Else it will not run in concurrent mode (where it can fire willUnmount twice etc)

It is cleaning out my jsx, but as argument to root.render there should be a react component

@David_Erenger I did try with createRoot also with strict mode like this (with no luck)

const container = document.getElementById('root') as HTMLElement;
const root = ReactDOMClient.createRoot(container);

root.render(<React.StrictMode>
  <App />
</React.StrictMode>);

With the release of React 18.1.0 we got the explanation to this:

Fix componentWillUnmount firing twice inside of Suspense

We use suspense for all content and I suspect Kate_Bultman does too?