2

I'm trying to update a chartjs using the useEffect hook.

However my react page is crashing saying:

TypeError: Cannot read property 'data' of undefined

Here is the code:

const MyChart = ({ chartData }: Props) => {
  const barChartData: Chart.ChartData = {
    labels: ["a", "b", "c", "d", "e", "f", "g", "h"],
    datasets: [
      {data: chartData},
    ],
  };
  const canvasRef = useRef<HTMLCanvasElement>(null);
  var myChart: Chart;

  useEffect(() => {
    const ctx = canvasRef.current?.getContext("2d");
    if (ctx) {
      var myChart = new Chart(ctx, {
        type: "radar",
        data: barChartData,
        options: { responsive: true },
      });
    }
  }, []);

  useEffect(() => {
    if (chartData != [0, 0, 0, 0, 0, 0, 0, 0]){
      
      const barChartDataUpdated: Chart.ChartData = {
        labels: ["a", "b", "c", "d", "e", "f", "g", "h"],
        datasets: [ { data: chartData } ],
      };
      myChart.data = barChartDataUpdated;
      myChart.update();
    }
  }, [chartData]);

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

From what I understand, the second useEffect ends up in a situation where it is triggered before the Chart is actually instantiated.

I tried to modify the code to put the update in the same useEffect as the myChart creation, and it works, but everytime chartData is created, a new Chart is created on top of the previous one, which is quite buggy.

  useEffect(() => {
    const ctx = canvasRef.current?.getContext("2d");
    if (ctx) {
      var myChart = new Chart(ctx, {
        type: "radar",
        data: barChartData,
        options: { responsive: true }
      });

      myChart.data.datasets = [ { data: chartData } ]
      myChart.update();
    }
  }, [chartData]);

I have also tried to create the object outside of useEffect, which doesnt work at all.

What is the proper way to update myChart when chartData is updated?

2 Answers 2

4
+50

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

Sign up to request clarification or add additional context in comments.

2 Comments

This is a very nice solution and at least it addresses the issue of the undefined chart, however there is a glitch. If you resize the window for example, you will see that the initial chart takes over the updated one. In your code sandbox simply click on change and move the panel separator you will see the glitch. I added a console log in the canvas and notice that the chart creation is called several times, hence what creates the issue.
Adding an extra condition for the instantiation fixes it: if (ctx && !chartRef.current)
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

1 Comment

Thanks, I believe you mean if(myChart && myChart.data). I tried that but myChart still remains undefined even when chartData is updated, and therefore the update in the if is never triggered.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.