开发者问题收集

Typescript 中的 ChartJs 图表:在 useEffect 中对象可能未定义

2021-03-21
2938

我正在尝试使用 useEffect 钩子更新 chartjs。

但是我的 react 页面崩溃了,显示:

TypeError: Cannot read property 'data' of undefined

这是代码:

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>
  );

据我所知,第二个 useEffect 最终处于在 Chart 实际实例化之前触发的情况。

我试图修改代码以将更新放在与 myChart 创建相同的 useEffect 中,并且它可以工作,但是每次创建 chartData 时,都会在前一个 Chart 之上创建一个新的 Chart,这有很多错误。

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]);

我也尝试过在 useEffect 之外创建对象,但根本不起作用。

更新 chartData 时更新 myChart 的正确方法是什么?

2个回答

我建议使用 回调引用 ,因为您正在处理需要元素才能实例化的第三方库。

回调引用是一个以元素为参数的函数。我们创建一个使用 canvas 调用的函数 canvasCallback ,并使用它来创建图表实例,我通过 useRef 而不是 useState 来存储它,因为它是可变的(尽管我认为这并不重要,因为所有重要的重新渲染都是由 chart.js 而不是 React 完成的)。

我们还需要一个 useEffect 钩子来检测来自 props 的数据的变化。由于您在此处以与之前相同的方式创建 Chart.ChartData 对象,因此我将该逻辑移至辅助函数 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

Linda Paiste
2021-03-24

这是因为当您的 chartData 未定义时,第二个 useEffect 也会触发。您的 if(chartData != [0, 0, 0, 0, 0, 0, 0, 0]) 始终为真,因为您正在比较引用,而不是原始值。您可以尝试 if(chartData && chartData.data) 来检查它们是否未定义

Joe's wiZa T
2021-03-23