Hello!
https://stackblitz.com/edit/react-kr5cmxga?file=app%2FcustomItem.tsx,app%2Fapp.tsx,resources.ts
CustomItem into the Scheduler, and inside this CustomItem, I use the standard SchedulerItem from the library. The Scheduler also includes a context menu Popup that opens on a right-click.useState value inside the Scheduler component. This causes the Scheduler to re-render, which in turn forces all of its child components—including all CustomItem instances—to re-render as well.CustomItem in React.memo (line 32 in the provided example). However, this did not solve the problem. All CustomItem components still re-render, which negatively impacts the application's performance.I have simplified this example for clarity. In my actual project, I pass more than just onContextMenuOpen into CustomItem, so my question goes beyond just fixing the popup behavior. Is there a way to prevent all CustomItem instances from re-rendering when the parent component updates, given that I use custom items and pass additional props into them?
Thanks!
1 Answer, 1 is accepted
Hello, Andrei,
The behavior observed is expected and stems from how React's `React.memo` works in combination with the Scheduler's internal rendering pipeline.
`React.memo` performs a shallow comparison of props. When the parent component re-renders (e.g., due to a `useState` update for the context menu), the Scheduler's internal components produce new object and function references for the props passed to custom items - such as event handlers (`onClick`, `onFocus`, etc.) and the `style` object. Since these are new references on every render, `React.memo`'s shallow comparison sees them as "changed" and triggers a re-render of the custom item regardless.
To prevent unnecessary re-renders of `CustomItem`, avoid passing values derived from changing state directly as props. You can use `useRef` instead of `useState` for values that don't need to trigger re-renders or to fully benefit from memoization by defining the wrapper component outside the render cycle like in the sample below.
This approach avoids prop drilling entirely and keeps the custom item's props stable:
import * as React from 'react';
import { Scheduler, WeekView } from '@progress/kendo-react-scheduler';
import { SchedulerItem } from '@progress/kendo-react-scheduler';
// 1. Create a context for the context menu handler
const ContextMenuContext = React.createContext<((e: React.MouseEvent, dataItem: any) => void) | null>(null);
// 2. Define CustomItem OUTSIDE the App component — this keeps the reference stable
const CustomItem = (props: any) => {
const onContextMenuOpen = React.useContext(ContextMenuContext);
const handleContextMenu = React.useCallback((e: React.MouseEvent) => {
e.preventDefault();
onContextMenuOpen?.(e, props.dataItem);
}, [onContextMenuOpen, props.dataItem]);
return (
<div onContextMenu={handleContextMenu}>
<SchedulerItem {...props} />
</div>
);
};
const App = () => {
const [menuVisible, setMenuVisible] = React.useState(false);
const contextMenuDataRef = React.useRef<any>(null);
// Stable callback via useCallback
const handleContextMenuOpen = React.useCallback((e: React.MouseEvent, dataItem: any) => {
contextMenuDataRef.current = { x: e.clientX, y: e.clientY, dataItem };
setMenuVisible(true);
}, []);
return (
<ContextMenuContext.Provider value={handleContextMenuOpen}>
<Scheduler
data={[/* ... */]}
item={CustomItem}
>
<WeekView />
</Scheduler>
{/* Render your Popup/context menu here using menuVisible and contextMenuDataRef.current */}
</ContextMenuContext.Provider>
);
};I hope the provided information will be helpful for you but let me know if I can assist you any further on this matter.
Regards,
Vessy
Progress Telerik
Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.
https://stackblitz.com/edit/react-kr5cmxga-kaqg3rdq?file=app%2Fapp.tsx
I followed all the recommendations, but the issue with all items re-rendering persists. This is clear from the console.log(`CustomItem "${props?.title}" is being rendered`); on line 38. Re-rendering of all items also occurs when simply left-clicking on any item or slot.
Hi, Andrew,
Thanks a lot for the provided sample. I examined it deeper and it seems that there are two distinct sources of re-renders, and each requires a targeted fix
First, you will need to use `React.memo` with a custom comparison function. The most effective mitigation is to tell `React.memo` exactly which props matter for `CustomItem`'s output. The comparison function should check only the event data fields and the `selected` flag, and ignore the function props and `style` that the Scheduler always recreates:
import * as React from "react";
import {
SchedulerItem,
SchedulerItemProps,
} from "@progress/kendo-react-scheduler";
// Fields to compare — adjust to match your actual data model fields
const arePropsEqual = (prev: SchedulerItemProps, next: SchedulerItemProps) => {
return (
prev.selected === next.selected &&
prev.tabIndex === next.tabIndex &&
prev.title === next.title &&
prev.start === next.start &&
prev.end === next.end &&
prev.dataItem === next.dataItem
);
};
// CustomItem defined OUTSIDE the parent component — keeps the reference stable
const CustomItem = React.memo((props: SchedulerItemProps) => {
console.log(`CustomItem "${props?.title}" is being rendered`);
return <SchedulerItem {...props} />;
}, arePropsEqual);
Second - isolate the Scheduler from external state changes by moving the Scheduler into a memoized wrapper component that receives only stable props. This prevents external state changes (like the context menu visibility flag) from ever reaching the Scheduler subtree:
// Context for the right-click handler
const ContextMenuContext = React.createContext<
((e: React.MouseEvent, dataItem: any) => void) | null
>(null);
// CustomItem with a custom memo comparator
const arePropsEqual = (prev: SchedulerItemProps, next: SchedulerItemProps) =>
prev.selected === next.selected &&
prev.tabIndex === next.tabIndex &&
prev.title === next.title &&
prev.start === next.start &&
prev.end === next.end &&
prev.dataItem === next.dataItem;
const CustomItem = React.memo((props: SchedulerItemProps) => {
const onContextMenuOpen = React.useContext(ContextMenuContext);
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
onContextMenuOpen?.(e, props.dataItem);
};
return (
<div onContextMenu={handleContextMenu}>
<SchedulerItem {...props} />
</div>
);
}, arePropsEqual);
// Memoized Scheduler wrapper re-renders only when data changes
interface SchedulerWrapperProps {
data: any[];
onDataChange: (e: SchedulerDataChangeEvent) => void;
}
const SchedulerWrapper = React.memo(
({ data, onDataChange }: SchedulerWrapperProps) => (
<Scheduler data={data} onDataChange={onDataChange} item={CustomItem}>
<WeekView />
</Scheduler>
),
);
const App = () => {
const [data, setData] = React.useState<any[]>([
/* your events */
]);
const [menuVisible, setMenuVisible] = React.useState(false);
const menuOffset = React.useRef({ left: 0, top: 0 });
const menuDataItem = React.useRef<any>(null);
const handleDataChange = React.useCallback((e: SchedulerDataChangeEvent) => {
setData(e.data);
}, []);
const handleContextMenuOpen = React.useCallback(
(e: React.MouseEvent, dataItem: any) => {
e.preventDefault();
menuOffset.current = { left: e.clientX, top: e.clientY };
menuDataItem.current = dataItem;
setMenuVisible(true);
},
[],
);
const handleClose = React.useCallback(() => setMenuVisible(false), []);
return (
<ContextMenuContext.Provider value={handleContextMenuOpen}>
<SchedulerWrapper data={data} onDataChange={handleDataChange} />
{menuVisible && (
<Popup offset={menuOffset.current} onClose={handleClose}>
{/* menu content */}
</Popup>
)}
</ContextMenuContext.Provider>
);
};I hope this will prove helpful. Let me know if you have additional details or requirements. I am happy to help explore further optimizations within the current Scheduler architecture.
Regards,
Vessy
Hello, Vessy!
https://stackblitz.com/edit/react-kr5cmxga-jh674xhp?file=app%2Fapp.tsx
I added arePropsEqual to check which props have changed, but if I only add the parameters you recommended, the items don't render. As far as I can tell, this happens because the item rendering data is grouped. I tried expanding the props being checked (below line 41), but I still see all items rerendering with every click. Unfortunately, adding SchedulerWrapper doesn't improve the situation either.
Thank you for the follow-up with the full code. After investigating the Scheduler's internal rendering pipeline in the context of grouped resources, two specific issues were found in the `arePropsEqual` implementation:
- `children` comparison always returns `false` -> `prev.children === next.children;`
- `style.visibility` and `style.display` must be compared; `style.top/left/width/height` should not
Updating the `arePropsEqual` declaration as follows should resolve the issue:
const arePropsEqual = (prev: SchedulerItemProps, next: SchedulerItemProps) => {
return (
prev.selected === next.selected &&
prev.tabIndex === next.tabIndex &&
prev.title === next.title &&
prev.start === next.start &&
prev.end === next.end &&
prev.dataItem === next.dataItem &&
// Required for grouped/positioned views: SchedulerViewItem transitions
// visibility from 'hidden' to undefined after DOM positioning.
// Without these, items remain invisible after the initial render.
prev.style?.visibility === next.style?.visibility &&
prev.style?.display === next.style?.display
);
};For convenience, I updated the provided sample as well, you can access it here - https://stackblitz.com/edit/react-kr5cmxga-brmjfeaq?file=app%2Fapp.tsx%3AL77
Hello Vessy,
Yes, that actually worked! The number of re-renders has decreased, thanks for the help. However, I still have a question: are there any plans to improve this so we can create a customItem or customSlot and pass props as usual, without any extra boilerplate? Right now, we have to pass props via context and wrap custom components in memo with custom prop comparison. Having to manually track all of this during component updates and extensions adds noticeable overhead.
Hi, Andrey,
Thanks a lot for the follow-up and the shared feedback. I will pass it to the Scheduler developers so they can consider such improvement. If you decide, you can also submit a feature request with this improvement explaining the exact change that you would expect from it, so it could gain popularity and be planned sooner, respectively.
