From 3b4db14424071aa78f7d85f2dbbdafe371ed0800 Mon Sep 17 00:00:00 2001 From: Andreas Weyer Date: Mon, 9 Feb 2026 19:58:15 +0000 Subject: [PATCH] Fix chart performance issues and duplicate keys - Memoize XAxisTick and YAxisTick renderers with useCallback - Remove Y-axis tickCount and allowDecimals=false to prevent duplicate keys - Add React.memo to SimulationChart with custom comparison - Remove unnecessary sorting after isFed and remove dose actions - Add handleActionWithoutSort for actions that don't affect order - Prevents double state updates that caused 'every other click' freezes --- src/components/day-schedule.tsx | 11 ++++++-- src/components/simulation-chart.tsx | 41 ++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/components/day-schedule.tsx b/src/components/day-schedule.tsx index 7daa460..4e8df9f 100644 --- a/src/components/day-schedule.tsx +++ b/src/components/day-schedule.tsx @@ -86,6 +86,7 @@ const DaySchedule: React.FC = ({ }, [scheduleSort]); // Wrap action handlers to cancel pending sorts and execute action, then sort + // Use this ONLY for actions that might affect dose order (like time changes) const handleActionWithSort = React.useCallback((dayId: string, action: () => void) => { // Cancel pending sort const pendingTimeout = pendingSorts.get(dayId); @@ -107,6 +108,12 @@ const DaySchedule: React.FC = ({ }, 50); }, [pendingSorts, onSortDoses]); + // Handle actions that DON'T affect dose order (no sorting needed) + // This prevents unnecessary double state updates and improves performance + const handleActionWithoutSort = React.useCallback((action: () => void) => { + action(); + }, []); + // Clean up pending timeouts on unmount React.useEffect(() => { return () => { @@ -437,7 +444,7 @@ const DaySchedule: React.FC = ({ {/* Action buttons - right aligned */}
handleActionWithSort(day.id, () => onUpdateDoseField(day.id, dose.id, 'isFed', !dose.isFed))} + onClick={() => handleActionWithoutSort(() => onUpdateDoseField(day.id, dose.id, 'isFed', !dose.isFed))} icon={} tooltip={dose.isFed ? t('doseWithFood') : t('doseFasted')} size="sm" @@ -445,7 +452,7 @@ const DaySchedule: React.FC = ({ className={`h-9 w-9 p-0 ${dose.isFed ? 'bg-orange-500 hover:bg-orange-600' : ''}`} /> handleActionWithSort(day.id, () => onRemoveDose(day.id, dose.id))} + onClick={() => handleActionWithoutSort(() => onRemoveDose(day.id, dose.id))} icon={} tooltip={t('removeDose')} size="sm" diff --git a/src/components/simulation-chart.tsx b/src/components/simulation-chart.tsx index 1c391c7..e5fc212 100644 --- a/src/components/simulation-chart.tsx +++ b/src/components/simulation-chart.tsx @@ -52,7 +52,7 @@ const CHART_COLORS = { cursor: '#6b7280' // gray-500 } as const; -const SimulationChart = ({ +const SimulationChart = React.memo(({ combinedProfile, templateProfile, chartView, @@ -173,7 +173,8 @@ const SimulationChart = ({ }, [totalHours, xTickInterval]); // Custom tick renderer for x-axis to handle 12h/24h/continuous formats and dark mode - const XAxisTick = (props: any) => { + // Memoized to prevent unnecessary re-renders + const XAxisTick = React.useCallback((props: any) => { const { x, y, payload } = props; const h = payload.value as number; let label: string; @@ -200,17 +201,18 @@ const SimulationChart = ({ {label} ); - }; + }, [showDayTimeOnXAxis, isDarkTheme, t]); - // Custom tick renderre for y-axis to handle dark mode - const YAxisTick = (props: any) => { + // Custom tick renderer for y-axis to handle dark mode + // Memoized to prevent unnecessary re-renders + const YAxisTick = React.useCallback((props: any) => { const { x, y, payload } = props; return ( {payload.value} ); - }; + }, [isDarkTheme]); // Calculate Y-axis domain based on data and user settings const yAxisDomain = React.useMemo(() => { @@ -529,9 +531,7 @@ const SimulationChart = ({ domain={yAxisDomain as any} axisLine={{ stroke: isDarkTheme ? '#ccc' : '#666' }} tick={} - tickCount={16} - interval={0} - allowDecimals={false} + allowDecimals={true} allowDataOverflow={false} />
); -}; +}, (prevProps, nextProps) => { + // Custom comparison function to prevent unnecessary re-renders + // Only re-render if relevant props actually changed + return ( + prevProps.combinedProfile === nextProps.combinedProfile && + prevProps.templateProfile === nextProps.templateProfile && + prevProps.chartView === nextProps.chartView && + prevProps.showDayTimeOnXAxis === nextProps.showDayTimeOnXAxis && + prevProps.showDayReferenceLines === nextProps.showDayReferenceLines && + prevProps.showIntakeTimeLines === nextProps.showIntakeTimeLines && + prevProps.showTherapeuticRange === nextProps.showTherapeuticRange && + prevProps.therapeuticRange?.min === nextProps.therapeuticRange?.min && + prevProps.therapeuticRange?.max === nextProps.therapeuticRange?.max && + prevProps.simulationDays === nextProps.simulationDays && + prevProps.displayedDays === nextProps.displayedDays && + prevProps.yAxisMin === nextProps.yAxisMin && + prevProps.yAxisMax === nextProps.yAxisMax && + prevProps.days === nextProps.days + ); +}); + +SimulationChart.displayName = 'SimulationChart'; export default SimulationChart;