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
This commit is contained in:
2026-02-09 19:58:15 +00:00
parent d544c7f3b3
commit 3b4db14424
2 changed files with 40 additions and 12 deletions

View File

@@ -86,6 +86,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
}, [scheduleSort]); }, [scheduleSort]);
// Wrap action handlers to cancel pending sorts and execute action, then sort // 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) => { const handleActionWithSort = React.useCallback((dayId: string, action: () => void) => {
// Cancel pending sort // Cancel pending sort
const pendingTimeout = pendingSorts.get(dayId); const pendingTimeout = pendingSorts.get(dayId);
@@ -107,6 +108,12 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
}, 50); }, 50);
}, [pendingSorts, onSortDoses]); }, [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 // Clean up pending timeouts on unmount
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
@@ -437,7 +444,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
{/* Action buttons - right aligned */} {/* Action buttons - right aligned */}
<div className="flex flex-nowrap items-center justify-end gap-1"> <div className="flex flex-nowrap items-center justify-end gap-1">
<IconButtonWithTooltip <IconButtonWithTooltip
onClick={() => handleActionWithSort(day.id, () => onUpdateDoseField(day.id, dose.id, 'isFed', !dose.isFed))} onClick={() => handleActionWithoutSort(() => onUpdateDoseField(day.id, dose.id, 'isFed', !dose.isFed))}
icon={<Utensils className="h-4 w-4" />} icon={<Utensils className="h-4 w-4" />}
tooltip={dose.isFed ? t('doseWithFood') : t('doseFasted')} tooltip={dose.isFed ? t('doseWithFood') : t('doseFasted')}
size="sm" size="sm"
@@ -445,7 +452,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
className={`h-9 w-9 p-0 ${dose.isFed ? 'bg-orange-500 hover:bg-orange-600' : ''}`} className={`h-9 w-9 p-0 ${dose.isFed ? 'bg-orange-500 hover:bg-orange-600' : ''}`}
/> />
<IconButtonWithTooltip <IconButtonWithTooltip
onClick={() => handleActionWithSort(day.id, () => onRemoveDose(day.id, dose.id))} onClick={() => handleActionWithoutSort(() => onRemoveDose(day.id, dose.id))}
icon={<Trash2 className="h-4 w-4" />} icon={<Trash2 className="h-4 w-4" />}
tooltip={t('removeDose')} tooltip={t('removeDose')}
size="sm" size="sm"

View File

@@ -52,7 +52,7 @@ const CHART_COLORS = {
cursor: '#6b7280' // gray-500 cursor: '#6b7280' // gray-500
} as const; } as const;
const SimulationChart = ({ const SimulationChart = React.memo(({
combinedProfile, combinedProfile,
templateProfile, templateProfile,
chartView, chartView,
@@ -173,7 +173,8 @@ const SimulationChart = ({
}, [totalHours, xTickInterval]); }, [totalHours, xTickInterval]);
// Custom tick renderer for x-axis to handle 12h/24h/continuous formats and dark mode // 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 { x, y, payload } = props;
const h = payload.value as number; const h = payload.value as number;
let label: string; let label: string;
@@ -200,17 +201,18 @@ const SimulationChart = ({
{label} {label}
</text> </text>
); );
}; }, [showDayTimeOnXAxis, isDarkTheme, t]);
// Custom tick renderre for y-axis to handle dark mode // Custom tick renderer for y-axis to handle dark mode
const YAxisTick = (props: any) => { // Memoized to prevent unnecessary re-renders
const YAxisTick = React.useCallback((props: any) => {
const { x, y, payload } = props; const { x, y, payload } = props;
return ( return (
<text x={x} y={y + 4} textAnchor="end" fill={isDarkTheme ? '#ccc' : '#666'}> <text x={x} y={y + 4} textAnchor="end" fill={isDarkTheme ? '#ccc' : '#666'}>
{payload.value} {payload.value}
</text> </text>
); );
}; }, [isDarkTheme]);
// Calculate Y-axis domain based on data and user settings // Calculate Y-axis domain based on data and user settings
const yAxisDomain = React.useMemo(() => { const yAxisDomain = React.useMemo(() => {
@@ -529,9 +531,7 @@ const SimulationChart = ({
domain={yAxisDomain as any} domain={yAxisDomain as any}
axisLine={{ stroke: isDarkTheme ? '#ccc' : '#666' }} axisLine={{ stroke: isDarkTheme ? '#ccc' : '#666' }}
tick={<YAxisTick />} tick={<YAxisTick />}
tickCount={16} allowDecimals={true}
interval={0}
allowDecimals={false}
allowDataOverflow={false} allowDataOverflow={false}
/> />
<RechartsTooltip <RechartsTooltip
@@ -760,6 +760,27 @@ const SimulationChart = ({
</div> </div>
</div> </div>
); );
}; }, (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; export default SimulationChart;