Skip to content

Commit 330fb1c

Browse files
authored
fix: improve and fix rerendering (#790)
* perf: reduce rerenders * test: added redraw story
1 parent 2f19eb3 commit 330fb1c

File tree

6 files changed

+180
-97
lines changed

6 files changed

+180
-97
lines changed

src/chart.tsx

Lines changed: 70 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import React, {
2-
useEffect,
3-
useState,
4-
useRef,
5-
useImperativeHandle,
6-
useMemo,
7-
forwardRef,
8-
} from 'react';
9-
import type { Ref, MouseEvent } from 'react';
1+
import React, { useEffect, useRef, useState, forwardRef } from 'react';
2+
import type { ForwardedRef, MouseEvent } from 'react';
103
import ChartJS from 'chart.js/auto';
114
import type { ChartData, ChartType, DefaultDataPoint } from 'chart.js';
125

13-
import { Props, ChartJSOrUndefined, TypedChartComponent } from './types';
6+
import type { Props, TypedChartComponent } from './types';
7+
import { reforwardRef, setNextDatasets } from './utils';
8+
9+
const noopData = {
10+
datasets: [],
11+
};
1412

1513
function ChartComponent<
1614
TType extends ChartType = ChartType,
@@ -22,7 +20,7 @@ function ChartComponent<
2220
width = 300,
2321
redraw = false,
2422
type,
25-
data,
23+
data: dataProp,
2624
options,
2725
plugins = [],
2826
getDatasetAtEvent,
@@ -32,45 +30,49 @@ function ChartComponent<
3230
onClick: onClickProp,
3331
...props
3432
}: Props<TType, TData, TLabel>,
35-
ref: Ref<ChartJS<TType, TData, TLabel>>
33+
ref: ForwardedRef<ChartJS<TType, TData, TLabel>>
3634
) {
37-
type TypedChartJS = ChartJSOrUndefined<TType, TData, TLabel>;
35+
type TypedChartJS = ChartJS<TType, TData, TLabel>;
3836
type TypedChartData = ChartData<TType, TData, TLabel>;
3937

40-
const canvas = useRef<HTMLCanvasElement>(null);
38+
const canvasRef = useRef<HTMLCanvasElement>(null);
39+
const chartRef = useRef<TypedChartJS | null>();
40+
/**
41+
* In case `dataProp` is function use internal state
42+
*/
43+
const [computedData, setComputedData] = useState<TypedChartData>();
44+
const data: TypedChartData =
45+
computedData || (typeof dataProp === 'function' ? noopData : dataProp);
4146

42-
const computedData = useMemo<TypedChartData>(() => {
43-
if (typeof data === 'function') {
44-
return canvas.current
45-
? data(canvas.current)
46-
: {
47-
datasets: [],
48-
};
49-
} else return data;
50-
}, [data, canvas.current]);
47+
const renderChart = () => {
48+
if (!canvasRef.current) return;
5149

52-
const [chart, setChart] = useState<TypedChartJS>();
50+
chartRef.current = new ChartJS(canvasRef.current, {
51+
type,
52+
data,
53+
options,
54+
plugins,
55+
});
5356

54-
useImperativeHandle<TypedChartJS, TypedChartJS>(ref, () => chart, [chart]);
57+
reforwardRef(ref, chartRef.current);
58+
};
5559

56-
const renderChart = () => {
57-
if (!canvas.current) return;
58-
59-
setChart(
60-
new ChartJS(canvas.current, {
61-
type,
62-
data: computedData,
63-
options,
64-
plugins,
65-
})
66-
);
60+
const destroyChart = () => {
61+
reforwardRef(ref, null);
62+
63+
if (chartRef.current) {
64+
chartRef.current.destroy();
65+
chartRef.current = null;
66+
}
6767
};
6868

6969
const onClick = (event: MouseEvent<HTMLCanvasElement>) => {
7070
if (onClickProp) {
7171
onClickProp(event);
7272
}
7373

74+
const { current: chart } = chartRef;
75+
7476
if (!chart) return;
7577

7678
getDatasetAtEvent &&
@@ -105,80 +107,54 @@ function ChartComponent<
105107
);
106108
};
107109

108-
const updateChart = () => {
109-
if (!chart) return;
110-
111-
if (options) {
112-
chart.options = { ...options };
110+
/**
111+
* In case `dataProp` is function,
112+
* then update internal state
113+
*/
114+
useEffect(() => {
115+
if (typeof dataProp === 'function' && canvasRef.current) {
116+
setComputedData(dataProp(canvasRef.current));
113117
}
118+
}, [dataProp]);
114119

115-
if (!chart.config.data) {
116-
chart.config.data = computedData;
117-
chart.update();
118-
return;
120+
useEffect(() => {
121+
if (!redraw && chartRef.current && options) {
122+
chartRef.current.options = { ...options };
119123
}
124+
}, [redraw, options]);
120125

121-
const { datasets: newDataSets = [], ...newChartData } = computedData;
122-
const { datasets: currentDataSets = [] } = chart.config.data;
123-
124-
// copy values
125-
Object.assign(chart.config.data, newChartData);
126-
127-
chart.config.data.datasets = newDataSets.map((newDataSet: any) => {
128-
// given the new set, find it's current match
129-
const currentDataSet = currentDataSets.find(
130-
d => d.label === newDataSet.label && d.type === newDataSet.type
131-
);
126+
useEffect(() => {
127+
if (!redraw && chartRef.current) {
128+
chartRef.current.config.data.labels = data.labels;
129+
}
130+
}, [redraw, data.labels]);
132131

133-
// There is no original to update, so simply add new one
134-
if (!currentDataSet || !newDataSet.data) return { ...newDataSet };
135-
136-
if (!currentDataSet.data) {
137-
// @ts-expect-error Need to refactor
138-
currentDataSet.data = [];
139-
} else {
140-
// @ts-expect-error Need to refactor
141-
currentDataSet.data.length = newDataSet.data.length;
142-
}
143-
144-
// copy in values
145-
Object.assign(currentDataSet.data, newDataSet.data);
146-
147-
// apply dataset changes, but keep copied data
148-
Object.assign(currentDataSet, {
149-
...newDataSet,
150-
data: currentDataSet.data,
151-
});
152-
return currentDataSet;
153-
});
132+
useEffect(() => {
133+
if (!redraw && chartRef.current && data.datasets) {
134+
setNextDatasets(chartRef.current.config.data, data.datasets);
135+
}
136+
}, [redraw, data.datasets]);
154137

155-
chart.update();
156-
};
138+
useEffect(() => {
139+
if (!chartRef.current) return;
157140

158-
const destroyChart = () => {
159-
if (chart) chart.destroy();
160-
};
141+
if (redraw) {
142+
destroyChart();
143+
setTimeout(renderChart);
144+
} else {
145+
chartRef.current.update();
146+
}
147+
}, [redraw, options, data.labels, data.datasets]);
161148

162149
useEffect(() => {
163150
renderChart();
164151

165152
return () => destroyChart();
166153
}, []);
167154

168-
useEffect(() => {
169-
if (redraw) {
170-
destroyChart();
171-
setTimeout(() => {
172-
renderChart();
173-
}, 0);
174-
} else {
175-
updateChart();
176-
}
177-
});
178-
179155
return (
180156
<canvas
181-
ref={canvas}
157+
ref={canvasRef}
182158
role='img'
183159
height={height}
184160
width={width}

src/utils.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ForwardedRef } from 'react';
2+
import type {
3+
ChartType,
4+
ChartData,
5+
DefaultDataPoint,
6+
ChartDataset,
7+
} from 'chart.js';
8+
9+
export function reforwardRef<T>(ref: ForwardedRef<T>, value: T) {
10+
if (typeof ref === 'function') {
11+
ref(value);
12+
} else if (ref) {
13+
ref.current = value;
14+
}
15+
}
16+
17+
export function setNextDatasets<
18+
TType extends ChartType = ChartType,
19+
TData = DefaultDataPoint<TType>,
20+
TLabel = unknown
21+
>(
22+
currentData: ChartData<TType, TData, TLabel>,
23+
nextDatasets: ChartDataset<TType, TData>[]
24+
) {
25+
currentData.datasets = nextDatasets.map(nextDataset => {
26+
// given the new set, find it's current match
27+
const currentDataset = currentData.datasets.find(
28+
dataset =>
29+
dataset.label === nextDataset.label && dataset.type === nextDataset.type
30+
);
31+
32+
// There is no original to update, so simply add new one
33+
if (!currentDataset || !nextDataset.data) return nextDataset;
34+
35+
Object.assign(currentDataset, nextDataset);
36+
37+
return currentDataset;
38+
});
39+
}

stories/Chart.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,10 @@ ClickEvents.argTypes = {
9393
onElementClick: { action: 'element clicked' },
9494
onElementsClick: { action: 'elements clicked' },
9595
};
96+
97+
export const Redraw = args => <Chart {...args} />;
98+
99+
Redraw.args = {
100+
data: data.multiTypeData,
101+
redraw: true,
102+
};

stories/Doughnut.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import { Doughnut } from '../src';
33
import { data } from './Doughnut.data';
44

@@ -19,3 +19,21 @@ export const Default = args => <Doughnut {...args} />;
1919
Default.args = {
2020
data,
2121
};
22+
23+
export const Rotation = args => {
24+
const [rotation, setRotation] = useState(0);
25+
26+
useEffect(() => {
27+
const interval = setInterval(() => {
28+
setRotation(rotation => rotation + 90);
29+
}, 3000);
30+
31+
return () => clearInterval(interval);
32+
});
33+
34+
return <Doughnut {...args} options={{ rotation }} />;
35+
};
36+
37+
Rotation.args = {
38+
data,
39+
};

stories/Pie.data.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import faker from 'faker';
2+
13
export const data = {
24
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
35
datasets: [
@@ -24,3 +26,10 @@ export const data = {
2426
},
2527
],
2628
};
29+
30+
export function randomDataset() {
31+
return {
32+
value: faker.datatype.number({ min: -100, max: 100 }),
33+
color: faker.internet.color(),
34+
};
35+
}

stories/Pie.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { Pie } from '../src';
3-
import { data } from './Pie.data';
3+
import { data, randomDataset } from './Pie.data';
44

55
export default {
66
title: 'Components/Pie',
@@ -19,3 +19,37 @@ export const Default = args => <Pie {...args} />;
1919
Default.args = {
2020
data,
2121
};
22+
23+
export const Dynamic = args => {
24+
const [datasets, setDatasets] = useState(() => [randomDataset()]);
25+
const onAdd = () => {
26+
setDatasets(datasets => [...datasets, randomDataset()]);
27+
};
28+
const onRemove = () => {
29+
setDatasets(datasets => datasets.slice(0, -1));
30+
};
31+
const data = {
32+
labels: datasets.map((_, i) => `#${i}`),
33+
datasets: [
34+
{
35+
data: datasets.map(({ value }) => value),
36+
backgroundColor: datasets.map(({ color }) => color),
37+
},
38+
],
39+
};
40+
41+
return (
42+
<>
43+
<Pie {...args} data={data} />
44+
<button onClick={onRemove}>Remove</button>
45+
<button onClick={onAdd}>Add</button>
46+
<ul>
47+
{datasets.map(({ value, color }, i) => (
48+
<li key={i} style={{ backgroundColor: color }}>
49+
{value}
50+
</li>
51+
))}
52+
</ul>
53+
</>
54+
);
55+
};

0 commit comments

Comments
 (0)