Updating Events/Invalids every minute

Hello everyone,
I need to update events and invalid ranges every minute. So far, I have come up with a solution that involves a timer running every minute to perform the desired updates. However, I’m having trouble with updating the invalid ranges. It always scrolls back to the top, and I can’t find a way to retain the user’s scroll position.

Here’s some context: We use the event calendar for a time tracking system, and employees can add events for up to five hours into the future. The core problem is that if a user keeps the page open, the set constraint doesn’t update. Eventually, the user is forced to reload the page.

Has anyone implemented a similar feature who might be able to help me?

Hi @Neklas,

Could you share a code example showing how you’re currently updating the events and the invalid ranges?

Hi @gabi,

please excuse my late response.
The following snippet is a simplified version of the code, as I’m unfortunately not able to share the full implementation.
It seems to work now, both in this minimal example and in the more complex version (at least I cannot reproduce the issue anymore).
If you happen to know a better way to achieve this, I’d really appreciate your suggestions.

import { useCallback, useEffect, useRef, useState } from 'react';

import { Eventcalendar } from '@mobiscroll/react';
import moment from 'moment';

const MINUTE_IN_MILLISECONDS = 60_000;

/**
 * @param {() => void} callback
 */
export function useOneMinuteTimer(callback) {
  const timeout = useRef(null);
  const interval = useRef(null);

  const isTimerRunning = useCallback(() => {
    return timeout.current !== null && interval.current !== null;
  }, []);

  const stopTimer = useCallback(() => {
    if (timeout.current !== null) {
      clearTimeout(timeout.current);
      timeout.current = null;
    }

    if (interval.current !== null) {
      clearInterval(interval.current);
      interval.current = null;
    }
  }, []);

  const startTimer = useCallback(() => {
    if (!isTimerRunning()) {
      timeout.current = setTimeout(() => {
        callback();

        interval.current = setInterval(() => {
          callback();
        }, MINUTE_IN_MILLISECONDS);
      }, MINUTE_IN_MILLISECONDS - new Date().getSeconds() * 1000 - new Date().getMilliseconds());
    }
  }, [isTimerRunning, callback]);

  useEffect(() => {
    startTimer();

    return () => {
      stopTimer();
    };
  }, [startTimer, stopTimer]);
}

export function CalendarExample() {
  const [invalid, setInvalid] = useState({
    start: moment().add(5, 'hours').toDate(),
    end: moment().add(5, 'hours').endOf('day').toDate(),
  });
  const [event, setEvent] = useState({
    start: moment().subtract(30, 'minutes').toDate(),
    end: moment().toDate(),
    text: 'Event',
  });
  const [selectedDate, setSelectedDate] = useState(new Date());

  useOneMinuteTimer(() => {
    const now = new Date();
    setEvent((ev) => ({
      ...ev,
      end: now,
    }));
    setInvalid((inv) => ({
      start: moment(now).add(5, 'hours').toDate(),
      end: moment(now).add(5, 'hours').endOf('day').toDate(),
    }));
  });

  return (
    <Eventcalendar
      selectedDate={selectedDate}
      onSelectedDateChange={({ date }) => setSelectedDate(date)}
      view={{
        schedule: {
          type: 'week',
          timeCellStep: 10,
          timeLabelStep: 5,
        },
      }}
      timeFormat="HH:mm"
      data={[event]}
      invalid={[
        {
          recurring: {
            repeat: 'daily',
            until: moment().subtract(6, 'days').toDate(),
            interval: 1,
          },
        },
        invalid,
      ]}
      dragTimeStep={5}
    />
  );
}

Hi @Neklas

The scroll reset happens because updating the invalid prop with new date objects triggers a full grid re-render. Two ways to fix this:

Approach 1: Keep invalid static, enforce the constraint in lifecycle events

Instead of recalculating invalid every minute, use onEventCreate / onEventUpdate to block events beyond the 5-hour window. This way invalid never changes, so no scroll reset:

// Track current time in a ref — no re-render on update
const nowRef = useRef(new Date());

// Static invalid that never changes — no scroll reset
const myInvalid = useMemo(() => [{
  recurring: { repeat: 'daily', until: moment().subtract(1, 'day').toDate(), interval: 1 },
}], []);

const handleEventCreate = useCallback((args) => {
  const cutoff = moment(nowRef.current).add(5, 'hours').toDate();
  if (args.event.start > cutoff) return false; // prevent creation
}, []);

// In your timer, only update nowRef and event data (data changes don't reset scroll):
useOneMinuteTimer(() => {
  nowRef.current = new Date();
  setEvents((prev) => prev.map((ev) => ({ ...ev, end: new Date() })));
});

Then pass onEventCreate={handleEventCreate} and onEventUpdate with the same logic. This mirrors the pattern from the Mobiscroll disallow past event creation demo — static invalids + lifecycle validation.

Approach 2: If you need the visual invalid shading to update, save/restore scroll manually

useOneMinuteTimer(() => {
  const scrollEl = document.querySelector('.mbsc-schedule-grid-scroll');
  const scrollTop = scrollEl?.scrollTop || 0;

  setInvalid({ /* ...updated range */ });

  requestAnimationFrame(() => {
    if (scrollEl) scrollEl.scrollTop = scrollTop;
  });
});

Approach 1 is cleaner — updating data doesn’t reset scroll, and the lifecycle handlers give you precise control without fighting the re-render cycle.