I'm struggling with the react-chartjs-2 component nested within a parent component. Whenever I set the useEffect or useState in another component, the chart flickers and undergoes excessive re-rendering, causing it to disappear altogether. I am using the latest version of react-chartjs-2 and chart.js package. I tried optimizing the performance by caching the chart component using useMemo and useCallback in another function and applying memo to the entire component, but the problem persists. Here is my code.
import { useRef, memo, useCallback, useMemo, useEffect } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ChartOptions,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { nanoid } from 'nanoid';
import useChart from '../hooks/useChart';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
);
const BarChart = (props: any): JSX.Element => {
const { width, height, data, labels, colors, showPercentage, showValue, position,
stacked, scaleLabelDisplay, scaleLabelLabel, autoParserDate, maxYAxis, otherSet } = props;
const chartRef = useRef<any>(null);
const { datasets } = useChart(chartRef, colors, data, otherSet);
const timeUnit = (autoParserDate) ? {
time: {
parser: 'YYYY-MM-DD',
tooltipFormat: 'll',
minUnit: 'day',
},
distribution: 'series',
} : {};
const barData = useMemo(() => ({
labels,
datasets,
}), [labels, datasets]);
const formatter = useCallback(
(value: any, ctx: any): any => {
if (showValue) return value === 0 ? null : value;
if (!value || !showPercentage) return null;
let sum = 0;
const dataArr = ctx.chart.data.datasets[0].data;
dataArr.map((data: any): void => {
sum += Number(data);
});
const percentage = `${(Number(value) * 100 / sum).toFixed(2)}%`;
return percentage;
}, [showValue, showPercentage],
);
const barOptions: ChartOptions<'bar'> = useMemo(() => ({
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position,
display: position ?? false,
},
tooltip: {
mode: 'index',
intersect: true,
},
datalabels: {
formatter,
color: showValue ? 'black' : 'white',
padding: 20,
},
},
scales: {
x: {
stacked: !!stacked,
beginAtZero: true,
min: 0,
ticks: {
callback(value: any, index: number, values: any) {
const displayLabel = this.getLabelForValue(value);
return `${displayLabel}`.length > 10 ? `${`${displayLabel}`.slice(0, 10)}...` : displayLabel;
},
},
...timeUnit,
title: {
display: scaleLabelDisplay ?? false,
text: scaleLabelLabel ?? 'Minute',
},
},
y: {
stacked: !!stacked,
beginAtZero: true,
ticks: {
callback(value: any, index: number, values: any) {
const displayLabel = this.getLabelForValue(value);
return `${displayLabel}`.length > 10 ? `${`${displayLabel}`.slice(0, 10)}...` : displayLabel;
},
},
min: 0,
max: maxYAxis,
},
},
}), [
labels,
datasets,
position,
stacked,
formatter,
showValue,
timeUnit,
scaleLabelDisplay,
scaleLabelLabel,
maxYAxis,
]);
return (
<Bar
redraw
datasetIdKey={nanoid()}
ref={chartRef}
data={barData}
options={barOptions}
width={width}
height={height}
plugins={[ChartDataLabels as any]}
/>
);
};
export default memo(BarChart);
and this is my custom hook useChart.tsx
import { useState, useEffect } from 'react';
import { nanoid } from 'nanoid';
const useChart = (chartRef: any, colors: any, data: any, otherSet?: any): any => {
const [datasets, setDatasets] = useState<any>([]);
useEffect((): void => {
if (chartRef.current) {
const chart = chartRef.current.getContext('2d');
const ctx = chart.chart.ctx as CanvasRenderingContext2D;
const facebook = ctx.createLinearGradient(0, 0, 400, 0);
facebook.addColorStop(0, '#0062FE');
facebook.addColorStop(1, '#02A9F9');
const orange = ctx.createLinearGradient(0, 0, 400, 0);
orange.addColorStop(0, '#FA7800');
orange.addColorStop(1, '#F9ED7D');
const purple = ctx.createLinearGradient(0, 0, 400, 0);
purple.addColorStop(0, '#2800EE');
purple.addColorStop(1, '#C099FF');
const lightGreen = ctx.createLinearGradient(0, 0, 400, 0);
lightGreen.addColorStop(0, '#00ED9E');
lightGreen.addColorStop(1, '#66FF7F');
const lemonGreen = ctx.createLinearGradient(0, 0, 400, 0);
lemonGreen.addColorStop(0, '#2DEE2D');
lemonGreen.addColorStop(1, '#C3FF4D');
const red = ctx.createLinearGradient(0, 0, 400, 0);
red.addColorStop(0, '#F50000');
red.addColorStop(1, '#FFC466');
const pink = ctx.createLinearGradient(0, 0, 400, 0);
pink.addColorStop(0, '#F5003B');
pink.addColorStop(1, '#FFB2D3');
const yellow = ctx.createLinearGradient(0, 0, 400, 0);
yellow.addColorStop(0, '#EEAE2D');
yellow.addColorStop(1, '#FFFF4D');
const violet = ctx.createLinearGradient(0, 0, 400, 0);
violet.addColorStop(0, '#BC2DEE');
violet.addColorStop(1, '#FF4DF8');
const sky = ctx.createLinearGradient(0, 0, 400, 0);
sky.addColorStop(0, '#4DA9FF');
sky.addColorStop(1, '#2DEECB');
const line = ctx.createLinearGradient(0, 0, 400, 0);
line.addColorStop(0, '#73E573');
line.addColorStop(1, '#00B906');
const lazada = ctx.createLinearGradient(0, 0, 400, 0);
lazada.addColorStop(0, '#F50092');
lazada.addColorStop(1, '#EE4D2D');
const shopee = ctx.createLinearGradient(0, 0, 400, 0);
shopee.addColorStop(0, '#EE4D2D');
shopee.addColorStop(1, '#FF5533');
const color = colors?.length ? colors.map((i: any) => {
switch (i) {
case 'orange':
return orange;
case 'purple':
return purple;
case 'lightGreen':
return lightGreen;
case 'lemonGreen':
return lemonGreen;
case 'red':
return red;
case 'pink':
return pink;
case 'yellow':
return yellow;
case 'violet':
return violet;
case 'sky':
return sky;
case 'line':
return line;
case 'lazada':
return lazada;
case 'shopee':
return shopee;
case 'facebook':
return facebook;
default:
return i;
}
}) : [facebook, orange, pink, purple, lightGreen, red, lemonGreen, yellow, violet, sky, line, lazada, shopee];
if (Array.isArray(data)) {
return setDatasets([
{
data,
label: nanoid(),
backgroundColor: color,
borderColor: color,
lineTension: 0,
barThickness: 'flex',
...otherSet,
},
]);
}
const datasets = Object.keys(data)?.map((i: any) => {
let color = facebook;
switch (data[i].color) {
case 'orange':
color = orange;
break;
case 'purple':
color = purple;
break;
case 'lightGreen':
color = lightGreen;
break;
case 'lemonGreen':
color = lemonGreen;
break;
case 'red':
color = red;
break;
case 'pink':
color = pink;
break;
case 'yellow':
color = yellow;
break;
case 'violet':
color = violet;
break;
case 'sky':
color = sky;
break;
case 'line':
color = line;
break;
case 'lazada':
color = lazada;
break;
case 'shopee':
color = shopee;
break;
case 'facebook':
color = facebook;
break;
default:
color = data[i].color;
break;
}
return {
label: data[i].label,
data: data[i].data,
backgroundColor: color,
tension: 0,
...data[i].otherSet,
};
});
setDatasets(datasets);
}
}, [data, colors, chartRef, otherSet]);
return { datasets };
};
export default useChart;