Skip to content

[scheduler] Calendar lazy-loading store-effect selector returns a new object every update #22763

@rita-codes

Description

@rita-codes

Summary

The Calendar premium lazy-loading plugin's store-effect selector returns a fresh object literal on every call, so — since registerStoreEffect compares with !== (reference equality) — the effect runs on every store update (selection, hover, drag placeholder moves…), not just when the visible days change. It's not a fetch storm (the effect guards on visibleDaysKey), but it wastes work on a hot path. The Timeline plugin does it correctly by returning a primitive string.

Details

x-scheduler-internals-premium/src/use-event-calendar-premium/plugins/EventCalendarPremiumLazyLoadingPlugin.ts:

store.registerStoreEffect(
  (state) => {
    const visibleDays = state.viewConfig?.visibleDaysSelector?.(state) ?? [];
    const visibleDaysKey = visibleDays.map((day) => day.key).join('|');
    return { viewConfig: state.viewConfig, visibleDaysKey, isLoading: state.isLoading }; // new object every call
  },
  (previous, next) => {
    if (previous.visibleDaysKey === next.visibleDaysKey) return;
    ...
  },
);

registerStoreEffect (SchedulerStore.ts):

if (nextValue !== previousValue) { effect(previousValue, nextValue); ... } // reference compare

A new object is never === the previous, so the effect runs on every notification. Per update it recomputes visibleDaysSelector(state) and rebuilds the joined key string — during a drag the store updates on every pointer move, so this churns each frame. (isLoading is included in the selected object but never used by the effect, adding extra re-runs.)

Compare the Timeline plugin, which returns a primitive and works correctly:

return `${state.adapter.getTime(viewConfig.start)}|${state.adapter.getTime(viewConfig.end)}`;

Suggested fix

Return a primitive key like the Timeline plugin — e.g. null when there's no viewConfig / not initialized, otherwise the visibleDaysKey string. The !== compare then only fires on real changes.

This also lets the instant-initial-load check use previousKey === null (as the Timeline does) instead of previous.viewConfig == null — which fixes the related issue where the Calendar's first lazy-load goes through the debounce instead of loading immediately. Worth fixing together.

Context

@mui/x-scheduler-internals-premium (master, pre-stable). Performance churn on a hot path; no correctness/fetch impact → Medium.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancescope: schedulerChanges related to the scheduler.type: enhancementIt’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions