I am currently using Graph.js to render graphs it is working on the initial render, but until I press setTimeformats buttons in order to show another graph on the same canvas, it is giving me Error: Canvas is already in use. Chart with ID ‘0’ must be destroyed before the canvas can be reused. Am I using it properly? How Should I destroy the chart in order to use other graphs on the same canvas? Help would be very appreciated.
import React, { useRef, useEffect, useState } from "react";
import { historyOptions } from "../chartConfig/chartConfig";
import Chart from 'chart.js/auto';
interface Props{
data:any
}
const ChartData:React.FC<Props> = ({ data}) => {
const chartRef = useRef<HTMLCanvasElement | null>(null);
const { day, week, year, detail } = data;
const [timeFormat, setTimeFormat] = useState("24h");
const determineTimeFormat = () => {
switch (timeFormat) {
case "24h":
return day;
case "7d":
return week;
case "1y":
return year;
default:
return day;
}
};
useEffect(() => {
if (chartRef && chartRef.current && detail) {
const chartInstance = new Chart(chartRef.current, {
type: "line",
data: {
datasets: [
{
label: `${detail.name} price`,
data: determineTimeFormat(),
backgroundColor: "rgba(174, 305, 194, 0.5)",
borderColor: "rgba(174, 305, 194, 0.4",
pointRadius: 0,
},
],
},
options: {
...historyOptions,
},
});
if (typeof chartInstance !== "undefined") chartInstance.destroy();
}
});
const renderPrice = () => {
if (detail) {
return (
<>
<p className="my-0">${detail.current_price.toFixed(2)}</p>
<p
className={
detail.price_change_24h < 0
? "text-danger my-0"
: "text-success my-0"
}
>
{detail.price_change_percentage_24h.toFixed(2)}%
</p>
</>
);
}
};
return (
<div className="bg-white border mt-2 rounded p-3">
<div>{renderPrice()}</div>
<div>
<canvas ref={chartRef} id="myChart" width={250} height={250}></canvas>
</div>
<div className="chart-button mt-1">
<button
onClick={() => setTimeFormat("24h")}
className="btn btn-outline-secondary btn-sm"
>
24h
</button>
<button
onClick={() => setTimeFormat("7d")}
className="btn btn-outline-secondary btn-sm mx-1"
>
7d
</button>
<button
onClick={() => setTimeFormat("1y")}
className="btn btn-outline-secondary btn-sm"
>
1y
</button>
</div>
</div>
);
};
export default ChartData;
Advertisement
Answer
One way you might solve this problem is by using a new state variable and useEffect to quickly remove and re-create the canvas element each time the timeFormat changes. Some key points here:
- As @CallumMorrisson mentioned, in order to understand this approach, it is extremely important to read and understand this section of the React docs about skipping the
useEffecthook in its entirety. - Using the
day,name,week,yearattributes directly inuseEffectinstead of the entiredatavariable makes sure that the chart instance is only re-created when necessary, not on every render. Same goes for the functiondetermineTimeFormat, those types of functions should be defined outside the component’s scope if possible.
const determineTimeFormat = (
timeFormat: string,
day: any,
week: any,
year: any
) => {
switch (timeFormat) {
case "24h":
return day;
case "7d":
return week;
case "1y":
return year;
default:
return day;
}
};
interface Props {
data: any
}
const ChartData: React.FC<Props> = ({ data }) => {
const chartCanvasRef = useRef<HTMLCanvasElement | null>(null);
const { day, week, year, detail } = data;
const { name } = detail;
const [timeFormat, setTimeFormat] = useState("24h");
const [isRebuildingCanvas, setIsRebuildingCanvas] = useState(false);
// remove the canvas whenever timeFormat changes
useEffect(() => {
setIsRebuildingCanvas(true);
}, [timeFormat]); // timeFormat must be present in deps array for this to work
/* if isRebuildingCanvas was true for the latest render,
it means the canvas element was just removed from the dom.
set it back to false to immediately re-create a new canvas */
useEffect(() => {
if (isRebuildingCanvas) {
setIsRebuildingCanvas(false);
}
}, [isRebuildingCanvas]);
useEffect(() => {
const chartCanvas = chartCanvasRef.current
if (isRebuildingCanvas || !chartCanvas) {
return;
}
const chartInstance = new Chart(chartRef.current, {
type: "line",
data: {
datasets: [
{
label: `${name} price`,
data: determineTimeFormat(timeFormat, day, week, year),
backgroundColor: "rgba(174, 305, 194, 0.5)",
borderColor: "rgba(174, 305, 194, 0.4",
pointRadius: 0,
},
],
},
options: {
...historyOptions,
},
});
return () => {
chartInstance.destroy();
}
}, [day, isRebuildingCanvas, name, timeFormat, week, year]);
return (
<>
{isRebuildingCanvas ? undefined : (
<canvas ref={chartCanvasRef} id='myChart' width={250} height={250} />
)}
<button onClick={() => setTimeFormat("24h")}>24h</button>
<button onClick={() => setTimeFormat("7d")}>7d</button>
<button onClick={() => setTimeFormat("1y")}>1y</button>
</>
);
};
export default ChartData;