title: React + D3 Force Layout: new Links have undefined positions
body: <p>I believe I've followed the general update pattern on new React Props. D3 does the data calculation and rendering when new props are received, so that React does not have to render every tick.</p>
  
  <p>D3 works well with a static layout. But when I receive new nodes and links at shouldComponentUpdate(nextProps) function, the nodes lack the following attributes: </p>
  
  <ul>
  <li>index - the zero-based index of the node within the nodes array. </li>
  <li>x - the x-coordinate of the current node position. </li>
  <li>y - the y-coordinate of the current node position.</li>
  </ul>
  
  <p>As a result, all new nodes have <code>&lt;g tranform=translate(undefined, undefined)/&gt;</code> and are clustered at the left top corner. </p>
  
  <p>The way I update the props are by pushing new objects to the nodes array and links array. I don't understand why D3 doesn't assign d.x and d.y as it did for the initial setup at componentDidMount(). I have been struggling with this problem for days. Hope someone can help me out here.</p>
  
  <p>Here is the ForceLayout.jsx: </p>
  
  <pre><code>//React for structure - D3 for data calculation - D3 for rendering
  
  import React from 'react';
  import * as d3 from 'd3';
  
  export default class ForceLayout extends React.Component{
    constructor(props){
      super(props);
    }
  
    componentDidMount(){ //only find the ref graph after rendering
      const nodes = this.props.nodes;
      const links = this.props.links;
      const width = this.props.width;
      const height = this.props.height;
  
      this.simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).distance(50))
        .force("charge", d3.forceManyBody().strength(-120))
        .force('center', d3.forceCenter(width / 2, height / 2));
  
      this.graph = d3.select(this.refs.graph);
  
      this.svg = d3.select('svg');
      this.svg.call(d3.zoom().on(
        "zoom", () =&gt; {
          this.graph.attr("transform", d3.event.transform)
        })
      );
  
      var node = this.graph.selectAll('.node')
        .data(nodes)
        .enter()
        .append('g')
        .attr("class", "node")
        .call(enterNode);
  
      var link = this.graph.selectAll('.link')
        .data(links)
        .enter()
        .call(enterLink);
  
      this.simulation.on('tick', () =&gt; {
        this.graph.call(updateGraph);
      });
    }
  
    shouldComponentUpdate(nextProps){
      //only allow d3 to re-render if the nodes and links props are different
      if(nextProps.nodes !== this.props.nodes || nextProps.links !== this.props.links){
        console.log('should only appear when updating graph');
  
        this.simulation.stop();
        this.graph = d3.select(this.refs.graph);
  
        var d3Nodes = this.graph.selectAll('.node')
          .data(nextProps.nodes);
        d3Nodes
          .enter() 
          .append('g')
          .attr("class", "node")
        .call(enterNode);
        d3Nodes.exit().remove(); //get nodes to be removed
        // d3Nodes.call(updateNode);
  
        var d3Links = this.graph.selectAll('.link')
          .data(nextProps.links);
        d3Links
          .enter()
          .call(enterLink);
        d3Links.exit().remove();
        // d3Links.call(updateLink);
  
        const newNodes = nextProps.nodes.slice(); //originally Object.assign({}, nextProps.nodes)
        const newLinks = nextProps.links.slice(); //originally Object.assign({}, nextProps.links)
        this.simulation.nodes(newNodes);
        this.simulation.force("link").links(newLinks);
  
        this.simulation.alpha(1).restart();
  
        this.simulation.on('tick', () =&gt; {
          this.graph.call(updateGraph);
        });
      }
      return false;
    }
  
    render(){
      return(
        &lt;svg
          width={this.props.width}
          height={this.props.height}
          style={this.props.style}&gt;
          &lt;g ref='graph' /&gt;
        &lt;/svg&gt;
      );
      }
    }
  
    /** d3 functions to manipulate attributes **/
    var enterNode = (selection) =&gt; {
      selection.append('circle')
        .attr('r', 10)
        .style('fill', '#888888')
        .style('stroke', '#fff')
        .style('stroke-width', 1.5);
  
      selection.append("text")
        .attr("x", function(d){return 20}) //
        .attr("dy", ".35em") // vertically centre text regardless of font size
        .text(function(d) { return d.word });
     };
  
     var enterLink = (selection) =&gt; {
        selection.append('line')
          .attr("class", "link")
          .style('stroke', '#999999')
          .style('stroke-opacity', 0.6);
     };
  
     var updateNode = (selection) =&gt; {
         selection.attr("transform", (d) =&gt; "translate(" + d.x + "," + d.y + ")");
      };
  
      var updateLink = (selection) =&gt; {
        selection.attr("x1", (d) =&gt; d.source.x)
          .attr("y1", (d) =&gt; d.source.y)
          .attr("x2", (d) =&gt; d.target.x)
          .attr("y2", (d) =&gt; d.target.y);
      };
  
      var updateGraph = (selection) =&gt; {
         selection.selectAll('.node')
            .call(updateNode);
         selection.selectAll('.link')
            .call(updateLink);
      };
  </code></pre>
  
  <p>I've tried pushing new node into the node array at shouldComponentUpdate() function, instead of modifying the arrays in server. But the new node still appears on the left top corner with undefined position. So I am guessing my problem is at shouldComponentUpdate(). Any help is greatly appreciated!!</p>
  
  <hr>
  
  <p>EDIT: found that Object.assign(...) does not return array. Changed it into array.slice() instead. Now all nodes are rendered with a position but not linked at all. Old nodes frayed from original positions as well.</p>
  
  <p><a href="https://i.stack.imgur.com/SOVNJ.png" rel="nofollow noreferrer">Here is how it looks when new props go in and shouldComponentUpdate is triggered</a></p>
  
  <p>I don't understand why the positions on the links don't correspond to the nodes.</p>