At Qumulo, we help our customers understand their storage capacity and performance by providing interactive analytics. We continuously improve our analytics; we listen to what our customers need, we track the right events in the system, and then deliver new user interface features to smartly organize and interact with this data. And we ship these new features every two weeks.

Shipping new web UI every two weeks means we can’t let our tools get in our way. We adopt new technology whenever it helps us go faster and write better code. When we started in 2012, we used Backbone, jQuery, and Underscore templates. Three years later, we’re all about React, D3.js, ES2015, and Babel, with a shift away from mutable Backbone models towards a Flux unidirectional dataflow architecture. Being able to evolve our codebase incrementally is critical to shipping regularly since we can’t just stop and rewrite our web app using the latest technologies.

Cool tech: React + D3.js

On their own, React and D3 are excellent JavaScript libraries. However, getting them to work well together can be a challenge. We tried several different design approaches over the last two years, and have finally found a design approach that leverages the strengths of each library.

So what’s so hard about using React with D3? The problem is in getting the libraries to cooperate in rendering elements to the screen in a way that lets you use all the power in each library. Let’s start with an example of a React component that is responsible for rendering a line chart. This was the very first iteration of our Capacity History feature, which shows how much cluster storage space has been used over time:

In this simple example, we have a time X-axis, a capacity Y-axis, and a line graph. We could factor this into four React components – a parent graph component that manages data for three child components:

let CapacityHistoryGraph = React.createClass({
    render() {
        return (
            <svg>
                <TimeAxis data={this.props.graphData} />
                <LineChart data={this.props.graphData} />
                <CapacityAxis data={this.props.graphData} />
            </svg>
        );
    }
});

Whenever the graphData prop changes on the parent component, React will call CapacityHistoryGraph’s render() function, which will send new props to the children. Here is a common approach you’ll see recommended for building a React component that wraps D3, using the LineChart as an example:

let LineChart = React.createClass({
    render() {
        return <g ref="lineChart" />;
    )

    componendDidMount() {
        // React rendered the <g> element, let D3 render the rest
        this.line = d3.svg.line()
            .x((d) => { return d.x; })
            .y((d) => { return d.y; });
        this.renderLineChart(this.props.data);
    }

    shouldComponentUpdate(nextProps) {
        // Let D3 update the chart, but prevent React from re-rendering
        this.renderLineChart(nextProps.data);
        return false;
    }

    renderLineChart(data) {
        d3.select(this.refs.lineChart)
            .removeAll()
             .append("path")
                .attr("d", this.line(data));
     }
});

Technically, this works; the line chart will render and update when props change. It’s kludgy, because you’re fighting React’s default lifecycle by suppressing rendering after the first render (see the highlighted “return false” statement). And as you build more React+D3 components, if any of them need to render child React components, you cannot send prop changes to them because they will never re-render. Component composability is a key tenet of React, and we didn’t want to give that up.

A better approach

After building enough components and experimenting with different approaches, we realized that there is a way to leverage the best parts of each framework without giving up features. First, let’s state our goals:

  1. React components must be composable for maximum reuse
  2. React components therefore must render and forward props to their children
  3. D3 is responsible for calculations (like figuring out where to draw points based on scale and viewport) during render
  4. React components include DOM elements based on D3 calculations as part of render whenever possible

So how do we fix the LineChart example from above to meet these goals? Instead of letting D3 create the <path> and assign points to it, we let React render the <path>, and let D3 fill in the list of points during render.

let LineChart = React.createClass({
     componentWillMount() {
         this.line = d3.svg.line()
             .x((d) => { return d.x; })
             .y((d) => { return d.y; });
     }

     render() {
         return (
             <g>
                 <path d={this.line(this.props.data)} />
             </g>
         );
     )
 });

As a result, look at how the LineChart component code is much cleaner and easier-to-read! Things are little more complicated for the time axis component because we want D3 to calculate the time scale, number of tick marks to display, and where to display them. d3.svg.axis does all of this work for us, but it needs an existing DOM element to render into. Therefore, we need to use React’s componentDidMount/componentDidUpdate lifecycle hooks to add elements to the DOM after React does its rendering pass.

let LineChart = React.createClass({
     componentWillMount() {
         this.line = d3.svg.line()
             .x((d) => { return d.x; })
             .y((d) => { return d.y; });
     }

     render() {
         return (
             <g>
                 <path d={this.line(this.props.data)} />
             </g>
         );
     )
 });

This approach has worked well for us. A quick recap:

  • Always let React components render
  • Create SVG elements in render ourselvesv
  • Leverage D3 for calculations, and for generating SVG elements when necessary

With that, our D3-based React components are easily reusable in different display contexts, and writing new components is straightforward. Finding a solution like this required experimentation and time to see what would work best. That’s all part of how we incrementally building software at Qumulo.

We’re engineers, builders, craftsmen and artists. We build products that are always on, that never lose data, and that store and retrieve data fast. Connect with us on social media!

Share with your network