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:
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user