DEV Community

Warren Wong
Warren Wong

Posted on • Updated on • Originally published at warrenwong.org

Best Practice for Integrating D3js in React

Creating custom data visualizations within a larger web app can become complicated when using D3.js since both React and and D3 want to handle DOM manipulation. D3 also includes the libraries to fetch and parse data, which maybe handled by React and then passed into the visualization component through props.

One of the better articles I've found regarding this is Elijah Meeks' Interactive Applications with React & D3. He also has a chapter that has a bit more breadth of examples in chapter 9 of his D3.js in Action book.

Meeks lays out the two common ways people usually integrate the two libraries which are basically to use React as a wrapper for the D3 visualization or using React to create each element in the svg. His preferred way is the latter because he likes to take advantage of the React lifecyle methods to update the elements of the visualization. The former example would require writing extra methods that the React lifecycle methods would then invoke whenever there's an update in the data or screen size, etc.

The obvious advantage of separating out the React and D3 is that you can stick fairly closely to any D3 example. You can also have D3 manage loading all the data that it's relying on.

I'm currently working with a team that isn't very familiar with front end development and React (which might be uncommon, since Meeks argues for using React for handing element creation since his team is more familiar with React than D3) thus the advantages of having use of React's lifecycle methods create the elements will be lost by having most of the team confused by intermingled logic.

Since I'm writing this in 2019, I want to have an example written with a Function Component utilizing Hooks rather than lifecycle methods.

You can follow along with his blog post mostly, but if you need his code example with useEffect, take a look below.

BarChart.js

import { max } from "d3-array";
import { scaleLinear } from "d3-scale";
import { select } from "d3-selection";
import React, { useEffect } from "react";

const BarChart = ({ data, size }) => {
  let node;
  const createBarChart = node => {
    const dataMax = max(data);
    const yScale = scaleLinear()
      .domain([0, dataMax])
      .range([size[1], 0]);

    select(node)
      .selectAll("rect")
      .data(data)
      .enter()
      .append("rect")
      .data(data)
      .style("fill", "#fe9922")
      .attr("x", (d, i) => i * 25)
      .attr("y", d => size[1] - yScale(d))
      .attr("height", d => yScale(d))
      .attr("width", 25);
  };

  useEffect(() => {
    createBarChart(node);
  });
  return <svg ref={n => (node = n)} height="500" width="500" />;
};

export default BarChart;
Enter fullscreen mode Exit fullscreen mode

The main differences here from the one that Meeks has in his post are:

  • useEffect invokes createBarChart and this is in effect the same result as invoking this function in both componentDidMount and componentDidUpdate.
  • I don't know why he appends all the "rect" elements then removes them, then adds them again. If anyone knows why, please let me know. I've chosen not to do that and instead call the data method immediately after append("rect").
  • I don't worry about the this context since the node is assigned to the node variable inside the BarChart function closure.
  • The lambda in useEffect can also return a function to do cleanup. Since the svg element is rendered by React, this isn't necessary for this case.
  • The contents of the createBarChart function can all just be in the lambda inside useEffect.

The WorldMap.js example is even more straight-forward, even though he now uses React to render each individual element in the svg.

WorldMap.js

import { geoMercator, geoPath } from "d3-geo";
import React from "react";
import "./App.css";
import worlddata from "./world"; // geojson

const WorldMap = () => {
  const projection = geoMercator();
  const path = geoPath().projection(projection);
  const countries = worlddata.features.map((d, i) => (
    <path key={`path${i}`} d={path(d)} className="countries" />
  ));

  return (
    <svg width={500} height={500}>
      {countries}
    </svg>
  );
};

export default WorldMap;
Enter fullscreen mode Exit fullscreen mode

This is pretty interesting that we don't need a reference to the svg node since we are using React to create the elements directly in the parent svg element returned by the WorldMap React component. I have to say that this does appeal to me. I can compose the visualization just like any other React component, but there are drawbacks. The performance of animations seems to be quite slow compared to having D3 handle that.

Originally published at warrenwong.org on March 11, 2019.

Oldest comments (0)