[Chartjs]-ChartJs chart in typescript: Object is possibly undefined in useEffect

4πŸ‘

βœ…

My recommendation is to use callback refs for you are dealing with third-party libraries that require an element in order to instantiate.

The callback ref is a function which takes the element as an argument. We create a function canvasCallback that gets called with the canvas and we use that to create the chart instance, which I am storing via useRef rather than useState since it is mutable (though I don’t think it really matters since all of the important re-rendering is done by chart.js rather than React).

We also need a useEffect hook to detect changes in the data from props. Since you are creating the Chart.ChartData object the same way here as before, I moved that logic into a helper function formatData

Component

import Chart from "chart.js";
import { useRef, useEffect, useState } from "react";

interface Props {
  chartData: number[];
}

const MyChart = ({ chartData }: Props) => {
  // helper function to format chart data since you do this twice
  const formatData = (data: number[]): Chart.ChartData => ({
    labels: ["a", "b", "c", "d", "e", "f", "g", "h"],
    datasets: [{ data }]
  });

  // use a ref to store the chart instance since it it mutable
  const chartRef = useRef<Chart | null>(null);

  // callback creates the chart on the canvas element
  const canvasCallback = (canvas: HTMLCanvasElement | null) => {
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (ctx) {
      chartRef.current = new Chart(ctx, {
        type: "radar",
        data: formatData(chartData),
        options: { responsive: true }
      });
    }
  };

  // effect to update the chart when props are updated
  useEffect(() => {
    // must verify that the chart exists
    const chart = chartRef.current;
    if (chart) {
      chart.data = formatData(chartData);
      chart.update();
    }
  }, [chartData]);

  return (
    <div className="self-center w-1/2">
      <div className="overflow-hidden">
        <canvas ref={canvasCallback}></canvas>
      </div>
    </div>
  );
};

Dummy tester

export default () => {
  const [data, setData] = useState([0, 1, 2, 3, 4, 5, 6, 7]);

  // want to see some changes in the props on order to make sure that MyChart updates
  const onClick = () => {
    setData((prevData) => prevData.slice(1).concat(10 * Math.random()));
  };

  return (
    <div>
      <button onClick={onClick}>Change</button>
      <MyChart chartData={data} />
    </div>
  );
};

Code Sandbox Link

0πŸ‘

It is because the second useEffect also triggers when your chartData is undefined. Your if(chartData != [0, 0, 0, 0, 0, 0, 0, 0]) is always true because you are comparing references, not primitive values.. You can try if(chartData && chartData.data) to check they are not undefined

Leave a comment