Отделить обновление logi c от useEffect в react + D3 - PullRequest
0 голосов
/ 28 мая 2020

Я пытаюсь построить визуализацию иерархического дерева с помощью React и D3. Мой компонент получает иерархические данные в формате CSV в качестве свойств, которые я передаю через функцию stratify D3, чтобы получить узел root моего дерева. Узел root установлен как состояние.

В настоящее время я использую один useEffect(()=>{},[root]) для построения дерева, которое повторно отображает любые изменения в root.

Это беспорядочно, поскольку все действия происходят внутри этого useEffect(). Я хочу знать, как я могу отделить метод update() и использовать его отдельно.

Поскольку я новичок, использую и React, и D3, я приветствую любые другие предложения о том, как обрабатывать состояние, как сделать он более декларативный и др.

Вот код:

useEffect(() => {
    if (root) {
      //Declare a tree layout
      //nodeSize ensure each node has it's own space and does not overlap
      const tree = d3
        .tree()
        .nodeSize([
          attributes.nodeWidth,
          attributes.nodeHeight + attributes.veritcalNodeGap,
        ]);

      root.x0 = 0;
      root.y0 = attributes.width / 2;

      //Set children of nodes deeper than 2 to null;
      root.descendants().forEach((d, i) => {
        d.id = i;
        d._children = d.children;
        if (d.depth && d.data.child.length !== 7) d.children = null;
      });

      // append the svg object to the body of the page
      // and define zoom behaviours

      const svg = d3
        .select(d3Ref.current)
        .call(
          d3
            .zoom()
            .scaleExtent([0.05, 3])
            .on("zoom", () => svg.attr("transform", d3.event.transform))
        )
        .on("dblclick.zoom", null)
        .append("svg")
        .attr("viewBox", [0, 0, attributes.width, attributes.height])
        .append("g")
        .attr("transform", (d) => `translate(${attributes.width / 2},120)`);

      //Group all links together
      const gLink = svg
        .append("g")
        .attr("fill", "none")
        .attr("stroke", "#555")
        .attr("stroke-opacity", 1)
        .attr("stroke-width", 1.5);
      // .attr("x", "200  ");

      //Group all nodes together
      const gNode = svg
        .append("g")
        .attr("cursor", "pointer")
        .attr("pointer-events", "all");

      const diagonal = linkVertical()
        .x((d) => d.x)
        .y((d) => d.y);

      update();

      function update() {
        const nodes = root.descendants().reverse();
        const links = root.links();

        tree(root);

        //Define group and join the data
        const node = gNode.selectAll("g").data(nodes, (d) => d.id);

        let nodeEnter = node
          .enter()
          .append("g")
          .attr("class", "node")
          .attr("transform", (d) => `translate(${root.x0},${root.y0})`)
          .on("click", (d) => {
            d.children = d.children ? null : d._children;
            update();
          });

        let nodeGroup = nodeEnter.append("g").attr("class", "node-group");

        nodeEnter
          .append("circle")
          .attr("r", 7)
          .attr("cursor", (d) => (d._children ? "pointer" : "none"))
          .attr("fill", (d) => (d._children ? "lightsteelblue" : "#999"))
          .attr("stroke", (d) => (d._children ? "steelblue" : "#999"))
          .attr("stroke-width", 2);
        //add text
        nodeEnter
          .append("text")
          .attr("dy", ".35em")
          .attr("x", 25)
          .text((d) => d.data.child);

        //Transition nodes to their new positions
        const nodeUpdate = node //SVG.data()
          .merge(nodeEnter)
          .transition()
          .duration(attributes.duration)
          .attr("transform", (d) => `translate(${d.x},${d.y})`)
          .attr("fill-opacity", 1)
          .attr("stroke-opacity", 1);

        //Transition exiting nodes to the parent's new position
        const nodeExit = node
          .exit()
          .transition()
          .duration(attributes.duration)
          .remove()
          .attr("transform", (d) => `translate(${root.x},${root.y})`);

        // // Update the links…
        const link = gLink.selectAll("path").data(links, (d) => d.target.id);
        // // Enter any new links at the parent's previous position.
        const linkEnter = link
          .enter()
          .append("path")
          .attr("class", "link")
          .attr("d", (d) => {
            const o = { x: root.x0, y: root.y0 };
            return diagonal({ source: o, target: o });
          });

        // //Transition links to their new position
        link
          .merge(linkEnter)
          .transition()
          .duration(attributes.duration)
          .attr("d", diagonal);

        // // Transition exiting nodes to the parent's new position.
        link
          .exit()
          .transition()
          .duration(attributes.duration)
          .remove()
          .attr("d", (d) => {
            const o = { x: root.x, y: root.y };
            return diagonal({ source: o, target: o });
          });

        root.eachBefore((d) => {
          d.x0 = d.x;
          d.y0 = d.y;
        });
      }
    }
  }, [root]);

1 Ответ

0 голосов
/ 29 мая 2020

Вот одна идея. Вы можете иметь начальный эффект onMount один раз, а затем обновлять только при изменении root. Вы можете использовать два useEffect на своем компоненте.

// hook on onMount, you prepare root, tree etc
let tree;
useEffect(() => {
  if (root) {
    //Declare a tree layout
    //nodeSize ensure each node has it's own space and does not overlap
    tree = d3
      .tree()
      .nodeSize([
        attributes.nodeWidth,
        attributes.nodeHeight + attributes.veritcalNodeGap,
      ]);

    root.x0 = 0;
    root.y0 = attributes.width / 2;

    //Set children of nodes deeper than 2 to null;
    root.descendants().forEach((d, i) => {
      d.id = i;
      d._children = d.children;
      if (d.depth && d.data.child.length !== 7) d.children = null;
    });

    // append the svg object to the body of the page
    // and define zoom behaviours

    const svg = d3
      .select(d3Ref.current)
      .call(
        d3
          .zoom()
          .scaleExtent([0.05, 3])
          .on('zoom', () => svg.attr('transform', d3.event.transform))
      )
      .on('dblclick.zoom', null)
      .append('svg')
      .attr('viewBox', [0, 0, attributes.width, attributes.height])
      .append('g')
      .attr('transform', (d) => `translate(${attributes.width / 2},120)`);

    //Group all links together
    const gLink = svg
      .append('g')
      .attr('fill', 'none')
      .attr('stroke', '#555')
      .attr('stroke-opacity', 1)
      .attr('stroke-width', 1.5);
    // .attr("x", "200  ");

    //Group all nodes together
    const gNode = svg
      .append('g')
      .attr('cursor', 'pointer')
      .attr('pointer-events', 'all');

    const diagonal = linkVertical()
      .x((d) => d.x)
      .y((d) => d.y);
  }
}, []);

// here is the update only when [root] changes

useEffect(() => {
  const nodes = root.descendants().reverse();
  const links = root.links();

  tree(root);

  //Define group and join the data
  const node = gNode.selectAll('g').data(nodes, (d) => d.id);

  let nodeEnter = node
    .enter()
    .append('g')
    .attr('class', 'node')
    .attr('transform', (d) => `translate(${root.x0},${root.y0})`)
    .on('click', (d) => {
      d.children = d.children ? null : d._children;
      update();
    });

  let nodeGroup = nodeEnter.append('g').attr('class', 'node-group');

  nodeEnter
    .append('circle')
    .attr('r', 7)
    .attr('cursor', (d) => (d._children ? 'pointer' : 'none'))
    .attr('fill', (d) => (d._children ? 'lightsteelblue' : '#999'))
    .attr('stroke', (d) => (d._children ? 'steelblue' : '#999'))
    .attr('stroke-width', 2);
  //add text
  nodeEnter
    .append('text')
    .attr('dy', '.35em')
    .attr('x', 25)
    .text((d) => d.data.child);

  //Transition nodes to their new positions
  const nodeUpdate = node //SVG.data()
    .merge(nodeEnter)
    .transition()
    .duration(attributes.duration)
    .attr('transform', (d) => `translate(${d.x},${d.y})`)
    .attr('fill-opacity', 1)
    .attr('stroke-opacity', 1);

  //Transition exiting nodes to the parent's new position
  const nodeExit = node
    .exit()
    .transition()
    .duration(attributes.duration)
    .remove()
    .attr('transform', (d) => `translate(${root.x},${root.y})`);

  // // Update the links…
  const link = gLink.selectAll('path').data(links, (d) => d.target.id);
  // // Enter any new links at the parent's previous position.
  const linkEnter = link
    .enter()
    .append('path')
    .attr('class', 'link')
    .attr('d', (d) => {
      const o = { x: root.x0, y: root.y0 };
      return diagonal({ source: o, target: o });
    });

  // //Transition links to their new position
  link
    .merge(linkEnter)
    .transition()
    .duration(attributes.duration)
    .attr('d', diagonal);

  // // Transition exiting nodes to the parent's new position.
  link
    .exit()
    .transition()
    .duration(attributes.duration)
    .remove()
    .attr('d', (d) => {
      const o = { x: root.x, y: root.y };
      return diagonal({ source: o, target: o });
    });

  root.eachBefore((d) => {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}, [root]);
...