Putting it together: D3 + React

We make our D3 idempotent so that it will behave like a component in our reactive framework.

In practice, though, we're going to enforce a strict boundary between our reactive component and our chart component.

Take this simple React component, which renders a chart container:

import React from 'react';

class MyReactComponent extends React.Component {
  render() {
    return (
      <section id="graphics">
        <div id="chart" />
      </section>
    );
  }
}

We'll use one of React's component lifecycle methods to introduce our D3 chart component.

(Nevermind the specifics of the chart component's API, for now. We'll cover that later.)

import React from 'react';
import * as d3 from 'd3';
import MyChart from './my-chart-component';


class MyReactComponent extends React.Component {
  // πŸ‘‰ Runs once right after the component is first created
  componentDidMount() {
    // πŸ‘‰ Create a new instance of the chart and attach it
    // to our component class.
    this.chart = new MyChart();

    // πŸ‘‰ Set the chart's root selection and call draw.
    this.chart.selection('#chart').draw();
  }

  render() {
    return (
      <section id="graphics">
        <div id="chart" />
      </section>
    );
  }
};

Here's what we've done:

  1. Let our React component render our chart container
  2. Once we're sure the container element has rendered -- i.e., in componentDidMount -- we call our D3 chart component
  3. Our chart component attaches to the chart container and renders our chart component inside

πŸ‘‰ Notice, we've created a hard boundary between React and D3. React doesn't know or care about anything below #chart, while D3 only operates within it.


So where's the idempotence come into play? Let's add some data to the mix.

We'll assume the data is passed down to our component through a prop that would be used like this:

<MyReactComponent data={myChartData} />

import React from 'react';
import * as d3 from 'd3';
import MyChart from './my-chart-component';


class MyReactComponent extends React.Component {
  componentDidMount() {
    this.chart = new MyChart();
    // πŸ‘‰ Pass data to our chart
    this.chart
      .selection('#chart')
      .data(this.props.data)
      .draw();
  }

  // πŸ‘‰ Runs every time we update the data passed to our component
  componentDidUpdate() {
    // πŸ‘‰ Runs again, but doesn't re-render! Why, it must be...
    // IDEMπŸ‘POπŸ‘TENTπŸ‘
    this.chart
      .data(this.props.data)
      .draw();
  }

  render() {
    return (
      <section id="graphics">
        <div id="chart" />
      </section>
    );
  }
};

Now, whenever the component updates/is passed new data, so does our chart.


⭐ Pro-tip: Use the diffing you get for free in most reactive frameworks to optimize your chart.

Passing chart data through your reactive component let's you take advantage of that component's smart optimization. Because React components will only update when new data is passed to them, your chart function will only be called when it needs to be.

Need more optimization? You can write your own shouldComponentUpdate method for finer control.

import differenceBy from 'lodash/differenceBy';

class MyReactComponent extends React.Component {
  // ...

  shouldComponentUpdate(nextProps) {
    // Some custom comparison between your current data
    // (this.props.data) and your updated data (nextProps.data).
    const difference = differenceBy(nextProps.data, this.props.data, 'id');
    return difference.length > 0; // Return true to update
  }

  componentDidUpdate() {
    // If shouldComponentUpdate was true, this is called
    this.chart
      .data(this.props.data)
      .draw();
  }

  // ...
};