This is the seventh day of my participation in the August More text Challenge. For details, see: August More Text Challenge

React provides a declarative way to clearly describe the UI, but what about third-party JS libraries such as d3.js that rely on real DOM nodes? Here are at least three things to think about:

  • Q: How do I get a native DOM node?

    A: Use ref to get A native DOM node reference.

  • Q: How do I update the state of a component on a native DOM node?

    A: For manual updates, React only maintains component state on virtual DOM nodes.

  • Note that DOM events on the native node are removed when the component is destroyed.

Let’s take d3.js as an example

What is d3.js?

D3 (Data-driven Documents or D3.js) is a very well known drawing JavaScript library for visualizing Data using Web standards. It must operate on the underlying DOM node, which is why d3.js was chosen. Second, its data-driven properties are very similar to React.

There is also a React library based on d3.js. Generally speaking, the functions are limited, and it is certainly not as complete as the functions obtained by using d3.js directly. Mastering the use of D3.js in React will be of great help to the development of visualization.

Create a simulated force model

Each Node can be dragged and has a force feedback effect. You can manually add new nodes to the middle circle.

The installation

  • yarn
yarn add d3
Copy the code
  • npm
npm install d3
Copy the code

Class component implementation

Create (componentDidMount)

On the home page, we define an initial set of data, including nodes and links:

{
  "nodes": [{"id": "id1"."group": 1
    },
    {
      "id": "id2"."group": 2
    },
    {
      "id": "id3"."group": 3
    },
    {
      "id": "id4"."group": 4}]."links": [{"source": "id1"."target": "id2"."value": 1
    },
    {
      "source": "id1"."target": "id3"."value": 1
    },
    {
      "source": "id1"."target": "id4"."value": 1}}]Copy the code

For D3, it requires a DOM Node as a drawing area. In React, references to real instances of components can be obtained by using the ref property, which can be set to a callback function. This is also highly recommended:

  • The callback function is executed immediately after the component is mounted, taking an instance of the component as an argument.

  • The callback is also executed immediately when the component is unloaded or the original REF property itself is changed, in which case the callback parameter is null to ensure memory leaks.

// d3Node is our drawing area
<div className="d3-node" ref={(node) = > (this.d3Node = node)} />
Copy the code

After the component is mounted, that is, componentDidMount, the d3Node initializes an SVG (including styles such as wide and long background colors and a mechanics simulation space). LinksGroup is a container for lines and nodesGroup is a container for nodes.

this.svg = d3
  .select(this.d3Node)
  .append('svg')
  .attr('width', width)
  .attr('height', height);
this.color = d3.scaleOrdinal(d3.schemeCategory10);
this.simulation = d3
  .forceSimulation()
  .force(
    'link',
    d3.forceLink().id((d) = > d.id),
  )
  .force('charge', d3.forceManyBody())
  .force('center', d3.forceCenter(width / 2, height / 2));

this.linksGroup = this.svg.append('g');
this.nodesGroup = this.svg.append('g');
Copy the code

The logic for rendering the data is the same for the first drawing and subsequent updates, so we can share this part of the code. The main logic for drawing the data is to draw as many circles as there are nodes, and the relationships between them are connected by lines. The positions of nodes are automatically generated by the D3.force API:

  updateDiagrarm() {
    const { data } = this.state;
    let link = this.linksGroup
      .attr('class'.'links')
      .selectAll('line')
      .data(data.links);
    link.exit().remove();
    link = link
      .enter()
      .append('line')
      .attr('stroke-width'.function (d) {
        return Math.sqrt(d.value);
      })
      .merge(link);

    let node = this.nodesGroup
      .attr('class'.'nodes')
      .selectAll('circle')
      .data(data.nodes);
    node.exit().remove();
    node = node
      .enter()
      .append('circle')
      .attr('r'.(d) = > (d.id === 'id1' ? 24 : 16))
      .attr('fill'.(d) = > {
        return this.color(d.group);
      })
      .call(
        d3
          .drag()
          .on('start'.this.dragstarted)
          .on('drag'.this.dragged)
          .on('end'.this.dragended),
      )
      .merge(node);

    this.simulation.nodes(data.nodes).on('tick', ticked);

    this.simulation.force('link').links(data.links).distance(100);

    this.simulation.alpha(1).restart();

    function ticked() {
      link
        .attr('stroke'.'#c7c7c7')
        .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);
    }
  }

  dragstarted = (event, d) = > {
    if(! event.active)this.simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  };
  dragged = (event, d) = > {
    d.fx = event.x;
    d.fy = event.y;
  };
  dragended = (event, d) = > {
    if(! event.active)this.simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  };
Copy the code

Update (componentDidUpdate)

Based on the D3 data-driven nature, the behavior of a new Node is the mapping between a new Node and a new Node in data:

handleAddNode = () = > {
  const id = `idThe ${new Date().getTime()}`;
  const node = { id, group: _.random(1.9)};this.setState({
    data: {
      nodes: [...this.state.data.nodes, node],
      links: [
        ...this.state.data.links,
        { source: 'id1'.target: id, value: 1},],}}); };Copy the code

Note that React only manages the d3Node layer. We manually manage the d3Node layer, so we add update logic to componentDidUpdate:

componentDidUpdate(prevProps, prevState) {
  if (this.state.data ! == prevState.data)this.updateDiagrarm();
}
Copy the code

The destruction

In this case, because there are no additional events bound to the real Dom, React removes the d3Node and everything else in the d3Node after component destruction.

Function component implementation

The drawing logic in React Hooks is the same as it is in class components.

  • Initialize the Svg

    useEffect(() = > {
      // initSvg} []);Copy the code
  • Update the view as data changes

    useEffect(() = > {
      // updateDiagrarm
    }, [data]);
    Copy the code

In practice, it was found that there are two pits to guard against:

  • To avoid repeated drawing, adding a second argument to useEffect [] does not completely avoid this problem. We can add another layer of judgment:

    const checkElementExist = (element) = > {
      if(element) { element.remove(); }}; checkElementExist(getSvg().selectAll('svg'));
    Copy the code
  • UseState does not retrieve the latest value until the react lifecycle is complete, which prevents manipulation of new Dom nodes until the end of the react lifecycle. We don’t need to use useState to update the view automatically, we can use useRef instead.

    const color = useRef(null);
    const simulation = useRef(null);
    const linksGroup = useRef(null);
    const nodesGroup = useRef(null);
    Copy the code

The complete code

If you want to run, you can poke here

React Best Practices

  • React Best Practices: How to implement native dialogs
  • React best practice: Drag and drop the sidebar
  • React best practice: Implement stepwise operations based on routing
  • React best practice: Handle multiple data sources
  • React Best practice: Complete a list requirement
  • React Best practice: Dynamic forms