React中使用D3加载数据时生成多个重复SVG元素问题求助
问题原因
你的判断方向基本正确,核心错误是把D3操作真实DOM的逻辑直接写在了组件渲染主流程里,既没控制执行时机,也没做重复内容清理,重复SVG的触发逻辑很明确:
- 组件首次加载时
data初始值为null,直接返回加载中的<pre>标签,此时页面根本不存在id为Wrapper的容器,D3选不到元素自然不会插入SVG - 接口请求完成后调用
setData触发重渲染,组件不再走加载分支,你写的选容器、追加SVG的逻辑开始执行,往Wrapper里插入第一个SVG - 如果你用的是React 18+开发环境,默认开启的严格模式会自动执行一次挂载→卸载→重挂载流程;后续只要有任何触发组件重渲染的动作(比如父组件更新、其他state变动),都会再次执行组件函数里的D3追加逻辑。而D3的
append()方法是往选中元素里新增内容,不会自动清空已有节点,每重渲染一次就多插一个SVG,自然会出现多个重复容器。 - 另外你现在的代码还有个时序隐患:D3选择
#Wrapper的逻辑写在return语句之前,这时候React还没把本次渲染生成的Wrapper div提交到真实DOM上,第一次拿到数据渲染时D3其实选不到元素,是靠后续的重渲染才把SVG加上的,属于典型的DOM操作和渲染生命周期不同步问题。
修复方法
- 所有涉及真实DOM操作的D3绘图逻辑,必须放到
useEffect钩子中执行,依赖项设为data,保证只有DOM挂载完成、数据就绪后才执行绘图 - 每次执行绘图逻辑前,先清空容器内的旧内容,从根源避免重复追加节点
- 推荐用
useRef获取DOM容器引用,代替id选择器,避免和页面其他同名id冲突,也更符合React的开发范式 - 可以在绘图相关的useEffect中返回清理函数,组件卸载时清空DOM、移除事件监听,避免内存泄漏
修复后的完整代码如下:
import React, { useState, useEffect, useRef } from 'react'; import * as d3 from 'd3'; const LineChart = () => { const weatherDataUrl = 'https://gist.githubusercontent.com/anujshaanmydash/17687a309a87105ab8991a3dcfd8c7e3/raw/weatherData.json'; const wrapperRef = useRef(null); const [data, setData] = useState(null); // 单独处理数据拉取 useEffect(() => { d3.json(weatherDataUrl).then(setData); }, []); // 单独处理D3绘图,依赖数据和DOM引用 useEffect(() => { // 数据或DOM没准备好时直接返回 if (!data || !wrapperRef.current) return; const yAccessor = (d) => d.temperatureMax; const dateParser = d3.timeParse('%Y-%m-%d'); const xAccessor = (d) => dateParser(d.date); const dimensions = { width: 1200, height: 500, margins: { top: 15, right: 15, bottom: 30, left: 50, }, }; dimensions.boundedWidth = dimensions.width - dimensions.margins.left - dimensions.margins.right; dimensions.boundedHeight = dimensions.height - dimensions.margins.top - dimensions.margins.bottom; // 核心:每次绘图前清空容器内所有旧节点,避免重复生成SVG const wrapperEl = d3.select(wrapperRef.current); wrapperEl.selectAll('*').remove(); const wrapper = wrapperEl .append('svg') .attr('width', dimensions.width) .attr('height', dimensions.height); const bounds = wrapper .append('g') .style( 'transform', `translate(${dimensions.margins.left}px,${dimensions.margins.top}px)` ); // 生成比例尺 const yScale = d3 .scaleLinear() .domain(d3.extent(data, yAccessor)) .range([dimensions.boundedHeight, 0]); const freezingTempArea = yScale(32); bounds .append('rect') .attr('x', 0) .attr('width', dimensions.boundedWidth) .attr('y', freezingTempArea) .attr('height', dimensions.boundedHeight - freezingTempArea) .attr('fill', '#e0f3f3'); const xScale = d3 .scaleTime() .domain(d3.extent(data, xAccessor)) .range(0, dimensions.boundedWidth); // 后续折线、坐标轴、交互逻辑都可以在这里继续写 }, [data]); if (!data) { return <pre>Loading....</pre>; } return <div ref={wrapperRef}></div>; }; export default LineChart;
内容的提问来源于stack exchange,提问作者Anuj Shaan
相关产品推荐
相关产品推荐

