首页 > 解决方案 > 无法实现 D3.js 以使用 useRef 进行反应。正确的方法是什么?

问题描述

我正在尝试使用 d3.js 创建一个折线图,并将其实现为带有钩子的函数组件。

我尝试使用useRef. 将它们初始化为 null,然后在 JSX 中设置它们,如您在代码中所见。当我想使用clientWidthwhich uses lineChartref 它说它是空的。

我想创建一个 initializeDrawing 函数来初始化空分配,但是由于组件不会在变量更改时刷新,所以它不起作用。我也想过携带这些变量,useState但这似乎不是一个好的解决方案。

我想向您学习使用功能组件实现 d3.js 图表的最佳解决方案是什么。

我正在尝试实现这个示例,但主要目标是学习实现 d3.js 以使用钩子反应功能组件的一般原则。

这是代码:

...
...
...
const LineChart: FunctionComponent = (props) => {
  let dataArrIndex = 0;
  const lineChart = useRef((null as unknown) as HTMLDivElement);
  const xAxis = useRef((null as unknown) as SVGGElement);
  const yAxis = useRef((null as unknown) as SVGGElement);
  const margin = { top: 40, right: 40, bottom: 40, left: 40 };
  const aspectRatio = 9 / 16;
  const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
  const chartHeight =
    lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
  const yScale = d3.scaleLinear().range([chartHeight, 0]);
  const xScale = d3.scaleBand().range([0, chartWidth]);
  const update = () => {
    yScale.domain([
      0,
      d3.max(
        data,
        (d): number => {
          return d[dataArrIndex].balance;
        },
      ) as number,
    ]);

    xScale.domain(data[dataArrIndex].map((d) => d.month));

    d3.select(yAxis.current).call(d3.axisLeft(yScale));
    d3.select(xAxis.current).call(d3.axisBottom(xScale));
  };
  return (
    <div className="line-chart" ref={lineChart}>
      <svg width={chartWidth} height={chartHeight}>
        <g transform={`translate(${margin.left},${margin.bottom})`} />
        <g ref={yAxis} />
        <g ref={xAxis} />
        <path />
      </svg>
    </div>
  );
};

export default LineChart;


标签: reactjstypescriptd3.js

解决方案


问题是您尝试在 dom 中创建此元素之前对元素使用 ref,因此您有 null 而不是对 htmlelement 的 ref。要解决您的问题,您需要将所有使用 refs 的代码包装到 useEffect 挂钩。

像这样的东西:

useEffect(() => {
  const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
  const chartHeight =
    lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
  const yScale = d3.scaleLinear().range([chartHeight, 0]);
  const xScale = d3.scaleBand().range([0, chartWidth]);
  const update = () => {
    yScale.domain([
      0,
      d3.max(
        data,
        (d): number => {
          return d[dataArrIndex].balance;
        },
      ) as number,
    ]);

    xScale.domain(data[dataArrIndex].map((d) => d.month));

    d3.select(yAxis.current).call(d3.axisLeft(yScale));
    d3.select(xAxis.current).call(d3.axisBottom(xScale));
  };
}

使用效果在功能组件中用作 componentDidMount/componentDidUpdate/componentWillUnmount 的类比

如果每次组件接收更新时都需要调用方法“update”,则需要将其从 useEffect 移动到函数体并在 ueEffect 中调用它:

const update = () => {
    yScale.domain([
      0,
      d3.max(
        data,
        (d): number => {
          return d[dataArrIndex].balance;
        },
      ) as number,
    ]);

    xScale.domain(data[dataArrIndex].map((d) => d.month));

    d3.select(yAxis.current).call(d3.axisLeft(yScale));
    d3.select(xAxis.current).call(d3.axisBottom(xScale));
  };

useEffect(() => {
  const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
  const chartHeight =
    lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
  const yScale = d3.scaleLinear().range([chartHeight, 0]);
  const xScale = d3.scaleBand().range([0, chartWidth]);
  update();
}

您可以通过多种方式使用宽度高度等属性:

  1. 使用 useState 钩子将其设置为状态: const { width, setWidth } = useState(0) 并在 useEffect 中从 ref 更新它

  2. 检查是否 ref !== null<svg width={chartRef && chartRef.current.offsetWidth} />


推荐阅读