Prevent unnecessary chart and board component re-renders

This commit is contained in:
Mike Cao 2026-02-12 16:30:43 -08:00
parent 1f0de47c01
commit 400a35d7af
4 changed files with 104 additions and 22 deletions

View file

@ -0,0 +1,17 @@
.column {
position: relative;
}
.columnAction {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease;
}
.column:hover .columnAction,
.column:focus-within .columnAction {
opacity: 1;
visibility: visible;
pointer-events: auto;
}

View file

@ -8,10 +8,11 @@ import {
Tooltip, Tooltip,
TooltipTrigger, TooltipTrigger,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { useBoard, useMessages } from '@/components/hooks'; import { useBoard, useMessages } from '@/components/hooks';
import { Pencil, Plus, X } from '@/components/icons'; import { Pencil, Plus, X } from '@/components/icons';
import type { BoardComponentConfig } from '@/lib/types'; import type { BoardComponentConfig } from '@/lib/types';
import styles from './BoardColumn.module.css';
import { BoardComponentRenderer } from './BoardComponentRenderer'; import { BoardComponentRenderer } from './BoardComponentRenderer';
import { BoardComponentSelect } from './BoardComponentSelect'; import { BoardComponentSelect } from './BoardComponentSelect';
@ -34,6 +35,13 @@ export function BoardColumn({
const { board } = useBoard(); const { board } = useBoard();
const { t, labels } = useMessages(); const { t, labels } = useMessages();
const websiteId = board?.parameters?.websiteId; const websiteId = board?.parameters?.websiteId;
const renderedComponent = useMemo(() => {
if (!component || !websiteId) {
return null;
}
return <BoardComponentRenderer config={component} websiteId={websiteId} />;
}, [component, websiteId]);
const handleSelect = (config: BoardComponentConfig) => { const handleSelect = (config: BoardComponentConfig) => {
onSetComponent?.(id, config); onSetComponent?.(id, config);
@ -50,11 +58,18 @@ export function BoardColumn({
justifyContent="center" justifyContent="center"
backgroundColor="surface-sunken" backgroundColor="surface-sunken"
position="relative" position="relative"
className={styles.column}
> >
{editing && canRemove && ( {editing && canRemove && (
<Box position="absolute" top="10px" right="20px" zIndex={100}> <Box
className={styles.columnAction}
position="absolute"
top="10px"
right="20px"
zIndex={100}
>
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
<Button variant="quiet" onPress={() => onRemove?.(id)}> <Button variant="outline" onPress={() => onRemove?.(id)}>
<Icon size="sm"> <Icon size="sm">
<X /> <X />
</Icon> </Icon>
@ -63,15 +78,21 @@ export function BoardColumn({
</TooltipTrigger> </TooltipTrigger>
</Box> </Box>
)} )}
{component && websiteId ? ( {renderedComponent ? (
<> <>
<Box width="100%" height="100%" overflow="auto"> <Box width="100%" height="100%" overflow="auto">
<BoardComponentRenderer config={component} websiteId={websiteId} /> {renderedComponent}
</Box> </Box>
{editing && ( {editing && (
<Box position="absolute" bottom="10px" right="20px" zIndex={100}> <Box
className={styles.columnAction}
position="absolute"
bottom="10px"
right="20px"
zIndex={100}
>
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
<Button variant="quiet" onPress={() => setShowSelect(true)}> <Button variant="outline" onPress={() => setShowSelect(true)}>
<Icon size="sm"> <Icon size="sm">
<Pencil /> <Pencil />
</Icon> </Icon>
@ -94,7 +115,7 @@ export function BoardColumn({
<Dialog <Dialog
title={t(labels.selectComponent)} title={t(labels.selectComponent)}
style={{ style={{
width: '750px', width: '1200px',
maxWidth: 'calc(100vw - 40px)', maxWidth: 'calc(100vw - 40px)',
maxHeight: 'calc(100dvh - 40px)', maxHeight: 'calc(100dvh - 40px)',
padding: '32px', padding: '32px',

View file

@ -1,8 +1,9 @@
import { Column, Text } from '@umami/react-zen'; import { Column, Text } from '@umami/react-zen';
import { memo } from 'react';
import type { BoardComponentConfig } from '@/lib/types'; import type { BoardComponentConfig } from '@/lib/types';
import { getComponentDefinition } from '../boardComponentRegistry'; import { getComponentDefinition } from '../boardComponentRegistry';
export function BoardComponentRenderer({ function BoardComponentRendererComponent({
config, config,
websiteId, websiteId,
}: { }: {
@ -23,3 +24,11 @@ export function BoardComponentRenderer({
return <Component websiteId={websiteId} {...config.props} />; return <Component websiteId={websiteId} {...config.props} />;
} }
export const BoardComponentRenderer = memo(
BoardComponentRendererComponent,
(prevProps, nextProps) =>
prevProps.websiteId === nextProps.websiteId && prevProps.config === nextProps.config,
);
BoardComponentRenderer.displayName = 'BoardComponentRenderer';

View file

@ -1,5 +1,5 @@
import { useTheme } from '@umami/react-zen'; import { useTheme } from '@umami/react-zen';
import { useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { Chart, type ChartProps } from '@/components/charts/Chart'; import { Chart, type ChartProps } from '@/components/charts/Chart';
import { ChartTooltip } from '@/components/charts/ChartTooltip'; import { ChartTooltip } from '@/components/charts/ChartTooltip';
import { useLocale } from '@/components/hooks'; import { useLocale } from '@/components/hooks';
@ -8,6 +8,8 @@ import { getThemeColors } from '@/lib/colors';
import { DATE_FORMATS, formatDate } from '@/lib/date'; import { DATE_FORMATS, formatDate } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { formatLongCurrency, formatLongNumber } from '@/lib/format';
const MemoChart = memo(Chart);
const dateFormats = { const dateFormats = {
millisecond: 'T', millisecond: 'T',
second: 'pp', second: 'pp',
@ -32,7 +34,13 @@ export interface BarChartProps extends ChartProps {
maxDate?: Date; maxDate?: Date;
} }
export function BarChart({ interface TooltipState {
title: string;
color?: string;
value: string;
}
function BarChartComponent({
chartData, chartData,
renderXLabel, renderXLabel,
renderYLabel, renderYLabel,
@ -45,7 +53,7 @@ export function BarChart({
currency, currency,
...props ...props
}: BarChartProps) { }: BarChartProps) {
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState<TooltipState | null>(null);
const { theme } = useTheme(); const { theme } = useTheme();
const { locale } = useLocale(); const { locale } = useLocale();
const { colors } = useMemo(() => getThemeColors(theme), [theme]); const { colors } = useMemo(() => getThemeColors(theme), [theme]);
@ -94,13 +102,23 @@ export function BarChart({
}, },
}, },
}; };
}, [chartData, colors, unit, stacked, renderXLabel, renderYLabel]); }, [
colors,
unit,
stacked,
renderXLabel,
renderYLabel,
minDate,
maxDate,
locale,
XAxisType,
YAxisType,
]);
const handleTooltip = ({ tooltip }: { tooltip: any }) => { const handleTooltip = useCallback(
({ tooltip }: { tooltip: any }) => {
const { opacity, labelColors, dataPoints } = tooltip; const { opacity, labelColors, dataPoints } = tooltip;
const nextTooltip = opacity
setTooltip(
opacity
? { ? {
title: formatDate( title: formatDate(
new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw), new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
@ -112,13 +130,26 @@ export function BarChart({
? formatLongCurrency(dataPoints[0].raw.y, currency) ? formatLongCurrency(dataPoints[0].raw.y, currency)
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`, : `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
} }
: null, : null;
setTooltip(prev => {
if (
prev?.title === nextTooltip?.title &&
prev?.color === nextTooltip?.color &&
prev?.value === nextTooltip?.value
) {
return prev;
}
return nextTooltip;
});
},
[currency, locale, unit],
); );
};
return ( return (
<> <>
<Chart <MemoChart
{...props} {...props}
type="bar" type="bar"
chartData={chartData} chartData={chartData}
@ -129,3 +160,7 @@ export function BarChart({
</> </>
); );
} }
export const BarChart = memo(BarChartComponent);
BarChart.displayName = 'BarChart';