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
useEffect
hook in its entirety. - Using the
day
,name
,week
,year
attributes directly inuseEffect
instead of the entiredata
variable 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;