You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React.StrictMode下如何重启D3力模拟解决拖拽失效问题

问题根因

React 18 严格模式在开发环境会执行组件双挂载流程:第一次挂载后立即触发卸载,再执行第二次挂载。当前useEffect没有编写清理逻辑,第一次执行时创建的D3力导向模拟实例、绑定的拖拽事件、追加到SVG内的图形元素都没有被销毁,第二次effect执行时会创建新的模拟实例、重复追加DOM节点,最终拖拽事件实际绑定在被覆盖的旧节点上,触发时操作的是已废弃的旧模拟实例,导致拖拽功能失效。

另外代码里存在几处笔误,也可能引发运行异常:

  • 碰撞力名称拼写错误:colide应为collide
  • 连线长度常量名拼写错误:LINK_LENGHT应为LINK_LENGTH
  • 节点fill属性行多写了一个闭合括号,会触发语法错误
修复方案

在useEffect中返回清理函数,在effect重执行、组件卸载前完成旧实例和DOM的销毁,修正后完整代码如下:

useEffect(function () {
  if (!svgRef.current) return;
  const svg = d3.select(svgRef.current);
  // 清空SVG内残留的旧元素,避免重复渲染
  svg.selectAll("*").remove();

  const nodes = d3.map(
    props.nodes,
    (node): Node => ({
      ...node,
    })
  );
  const links = d3
    .map(props.links, (link) => ({
      source: nodes.find((node) => node.id === link.source),
      target: nodes.find((node) => node.id === link.target),
    }))
    .filter(
      (link): link is { source: Node; target: Node } =>
        link.source !== undefined && link.target !== undefined
    );

  const simulation = d3
    .forceSimulation(nodes)
    .force(
      "link",
      d3
        .forceLink(links)
        .id((_, i) => nodes[i].id)
        .distance(LINK_LENGTH)
    )
    .force("charge", d3.forceManyBody().strength(-NODE_REPULSION))
    .force("center", d3.forceCenter())
    .force("forceX", d3.forceX().strength(NODE_GRAVITY))
    .force("forceY", d3.forceY().strength(NODE_GRAVITY))
    .force("collide", d3.forceCollide(NODE_RADIUS * NODE_MARGIN))
    .on("tick", ticked);

  const link = svg
    .selectAll("line")
    .data(links)
    .enter()
    .append("line")
    .style("stroke", "#aaa");

  const node = svg
    .selectAll("circle")
    .data(nodes)
    .enter()
    .append("circle")
    .call(
      d3
        .drag<SVGCircleElement, Node>()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended)
    )
    .attr("fill", (node) => node.color)
    .attr("r", NODE_RADIUS)
    .attr("stroke", "#000000")
    .attr("stroke-width", 1);

  function ticked() {
    link
      .attr("x1", (d) => d.source.x!)
      .attr("y1", (d) => d.source.y!)
      .attr("x2", (d) => d.target.x!)
      .attr("y2", (d) => d.target.y!);

    node.attr("cx", (d) => d.x!).attr("cy", (d) => d.y!);
  }

  function dragstarted(event: any, d: Node) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(event: any, d: Node) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function dragended(event: any, d: Node) {
    if (!event.active) simulation.alphaTarget(0.0001);
    d.fx = null;
    d.fy = null;
  }

  // 清理逻辑
  return () => {
    // 停止力模拟,释放实例
    simulation.stop();
    // 清除所有渲染的图形和绑定的事件
    svg.selectAll("*").remove();
  };
},
[props.nodes.length, props.links.length]
)
后续排查思路

如果修改后仍存在问题,按以下顺序校验:

  • 检查effect依赖项:当前仅监听节点、连线数组长度,如果业务中存在数组长度不变但内容更新的场景,依赖项不会触发effect重跑,会导致数据与渲染不一致,这类场景可以直接将props.nodes、props.links作为依赖项,或使用稳定的序列化值作为依赖
  • 校验事件触发:在dragged回调中打印日志,如果拖拽时没有日志输出,说明事件绑定被其他逻辑覆盖;如果有日志输出但节点不移动,说明当前操作的是错误的模拟实例
  • 检查ref占用:确认svgRef没有被其他组件/逻辑同时操作,避免DOM被意外修改

内容的提问来源于stack exchange,提问作者Jerico

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 16:33:28