DEV Community

Cover image for React + D3.js: Balancing Performance & Developer Experience
Thibaut Tiberghien
Thibaut Tiberghien

Posted on • Edited on

React + D3.js: Balancing Performance & Developer Experience

Originally posted on Medium on May 17, 2017.

Let’s put it out there, I love dashboards. I find the way they help you gain a rapid understanding out of complex information really interesting. I have written real-time data visualisations in the past, but always thought that complete dashboards were really hard to implement. That was until I learnt React a while back, and then it clicked: I had finally found a tech that would make building dashboards easier and save the developer’s sanity. I recently started on a side project to try and get React and D3 to integrate seamlessly, with two main goals: render performance and developer experience (DX).

Below is a quick showcase of the mock dashboard I built. The data here is not important since the focus is really on the technical integration of both libraries. You can find a live demo hosted on ∆ now and the code on GitHub.

mock dashboard

Motivation

There are many great things about integrating React and D3. You can build yourself a custom library of reusable charts backed by React, which means better render performance with React’s reconciliation, and opportunities for lifecycle performance tweaks. Additionally, you bring all the new shiny possibilities of the modern web to your D3 charts: great developer tools, server-side rendering, an array of excellent state management options, data selectors and immutability, CSS-in-JS, …

Of course, you can use some of these things without React but it is so much easier when the path is all tar road with proper documentation.

The problem

The problem lies with integrating React and D3 seamlessly. Both libraries are built upon data-driven DOM manipulation where the DOM is taken care of for you. So without careful precautions, React would not accept well getting its stuff moved around. It’s like trying to change some code convention in a project managed by that OCD colleague of yours (that might be me). Ouch!

So I read a bit here and there and compared the options available. Below is what I found and thought. I hope you will find this interesting or even helpful. I’m still learning all this, so do drop a response whether you want to send appreciation, highlight a misunderstanding on my end, or point me in a better direction.


React + D3: What is the best way?

TL;DR: Looking into integrating React and D3 seamlessly, I first tried to disable React in D3 land (1), then to use D3’s helpers only and to render charts as JSX with React (2), to finally settle on the react-faux-dom approach (3).

Solution 1 — To each its (DOM) land!

The first and simplest idea I have encountered is to basically disable React in D3 land. There are a few techniques to do so, either by rendering an empty <div/> with React which becomes D3’s scope, or by returning false in the shouldComponentUpdate() lifecycle method.

My main issue with this rather effective solution is that you lose all the goodness brought by React into D3 land. In particular, you get slower rendering performance by doing heavy DOM manipulation that React’s reconciliation algorithm could have shaved milliseconds off. You also loose all the tooling and the DX provided by React that you probably started to love (see Motivation). And for the last point, I will just go ahead and quote Oliver Caldwell, whom I completely agree with.

Many solutions involve stepping out of the React tree for that specific component, which does work, but leaves you with a little island of mutable DOM, festering away inside your tree. It just doesn’t feel quite right to me.
— Oliver Caldwell, D3 within React the right way

Solution 2 — D3 for the maths, React for the DOM

This is probably the most widespread technique at the time of writing. It consists in only using D3’s helpers to prepare the data, the axes, etc. and then feed all of that to React to be rendered. This means you don’t actually use D3’s data binding, but handle it yourself with React by specifying a key for all your SVG elements. This is something you sometimes have to do in D3 too, when the binding is not trivial enough for D3 to figure it out alone. The big change here is that you will render your SVG elements as JSX, instead of using the familiar d3.(...).append(). There is a great post by Dan Scanlon on Hackernoon about this approach.

This method provides good performance overall, but my main issues here are with the DX. Firstly, the visualization code is extremely different from vanilla D3 code. This introduces several disavantages in my opinion.

  • Having to draw out the SVG and axes myself in JSX feels really awkward at first, I’m not sure how long I would take to get used to it and whether I would ever like this way of doing things.
  • It undeniably stamps your code “React”, making it harder to extract it from its component in case it ever becomes useful. I worry here about framework lock-in, since the volatility of JS frameworks is rather high as compared to D3's.
  • It becomes time-consuming to code from example (or port existing code), since you have to convert all your vanilla D3 code to JSX. This is important for me as it is my default process for implementing D3 visualizations, and I’m probably not alone considering the 20K+ examples available.
  • The learning curve for D3 developers is steep and I am not sure if it is worth the cost, at least not for every team.

Another issue with this solution is that since D3’s data binding is not used, we also lose the enter-update-exit pattern and therefore D3 transitions. I consider D3 transitions and animations as a great part of D3's value proposition. This is what powers many techniques for creating rich user experiences. This, added to the reduced DX, makes it hard for me to really embrace this approach.

Solution 2b — Enter/exit with React, update with D3

This solution was described in an excellent Medium post by Shirley Wu. It builds upon solution 2 but mixes in a bit of solution 1. The idea is still to use D3’s helpers and JSX to render SVG elements, except that now the elements rendered by React are rendered without attributes, and D3 is used to add their attributes. So the line of ownership between React and D3 is not at the element level like in solution 1, but at the attributes level. Although small, the difference is key to getting D3 transitions back. Attributes being handled by D3, we can add an enter() method called in componentDidMount() and an update() method called in componentDidUpdate(). Each of these methods can use typical D3 code to position, style, and transition elements.

There are some caveats to this approach:

  • As declared in the post introducing this idea, exit() transitions are not supported without bringing in React’s TransitionGroup.
  • Since React does not keep track of attributes, we have to manually implement state comparison to detect when the component should update in order to call the update() method performing D3 transitions. This basically means that we implement React’s job for it because we intentionaly bypassed it.
  • This approach still has all the DX issues inherent to solution 2.
  • I found the implementation too complex for a simple chart. I believe this is due to the need to split the code according to the line of ownership between React and D3, instead of splitting it into logical units.

Solution 3 — Feed D3 a fake DOM that renders to state

This is the solution I found the most elegant so far, and it is what powers the demo at the beginning of this post. It is based on react-faux-dom, made by Oliver Caldwell who detailed the idea on his blog. The concept is that D3 is fed a fake DOM which implements all methods it would expect the DOM to have. That fake DOM is manipulated by D3 and then automatically rendered as React elements stored into the component’s state where React can pick up changes and kick-off an update, including lifecycle methods and reconciliation as you would expect.

I found this approach elegant because both D3 and React are used without alienation.

  • Except for feeding the faux DOM node to D3 instead of using a selector as you normally would, vanilla D3 code can be used. This means no framework lock-in, easily port existing code or start from example, and no learning curve for D3 developers.
  • The full D3 API is supported, with transitions, animations, mouse events, etc.
  • React’s component lifecycle and render methods are being used, and changes made by the D3 are picked up and reconciled seamlessly. Hence, you get to enjoy the typical render performance of React components.
  • SVG elements are automatically transformed into React elements and are inspectable in the devtools.
  • The implementation is compatible with server-side rendering, so you get isomorphic charts at no cost.

Overall, this solution has restored my faith is having a great DX when using D3 visualizations in React components, while making the most out of React’s render performance.


Performance tricks

In this section, I will describe some techniques I have used to improve the render performance of my playground dashboard. The basic idea is that D3 updates are more expensive than React re-renders. Indeed, without resorting to performance-motivated tricks to decompose you D3 code, each time D3 processes some update it needs to recompute all the chart helpers and check all the data to possibly update the bound elements. Also D3 updates will trigger a new render cycle of the component anyway. So how can we avoid D3 updates? TL;DR: Only update D3 on new data or on resize events.

Extract tooltips to React

Tooltips are typically something I prefer extracting from D3 into React land. Being usually displayed on mouse hover and hidden on mouse out, their update rate is much higher than that of the underlying data. This means recomputing helpers and checking over the data is pointless and it makes tooltips prime candidates for Reactification — if that’s even a word.

To extract tooltips to React, I add mouseover and mouseout event listeners to SVG elements, in which I setState the hover value so that React can kick-off a render cycle on updates. I often use setTimeout() in the mouseout callback, and then clearTimeout() in the mouseover callback to avoid the flickering between hovers caused by the margin/space between the graph elements. This also lets me use CSS animations to translate tooltips. The tooltip is then rendered directly in JSX, using D3 helpers for positioning if necessary. You can simply share the helpers in the component’s scope using the this keyword. Also, we must be careful to avoid updating D3 when the hover changes in state. To do so, I omit hover from the state’s shallow comparison done in componentDidUpdate. Now, that’s a lot to take in without code so here you go with a simplified code excerpt and feel free to dive in the full source on GitHub.

class Chart extends React.Component {
  // ...
  componentDidUpdate (prevProps, prevState) {
    const stripState = p => _.omit(p, ['hover'])
    if (!shallowEqual(stripState(this.state), stripState(prevState))) {
      this.renderD3()
    }
  }
  // ...
  setHover (hX) {
    this.setState({
      hover: hX
    })
  }
  // ...
  computeTooltipProps (hX) {
    const hoveredData = _.map(this.props.data, 'values').map(d =>
      _.find(d, {x: hX})
    )
    return {
      style: {
        top: this.y(_.sum(_.map(hoveredData, 'y'))),
        left: this.x(hX)
      },
      content: `${hX}: ${_.map(hoveredData, 'y').join(', ')}`
    }
  }
  // ...
  render () {
    return (
      <div>
        {this.state.chart}
        {this.state.hover &&
          <Tooltip {...this.computeTooltipProps(this.state.hover)} />
        }
      </div>
    )
  }
  // ...
  renderD3() {
    // ...
    // make x and y helpers available to JSX for tooltips positioning
    const x = d3.scale
      .ordinal()
      .domain(this.props.xDomain)
      .rangeRoundBands([0, width], 0.08)
    this.x = x
    const y = d3.scale.linear().domain([0, yStackMax]).range([height, 0])
    this.y = y
    // ...
    // add mouse event listeners
    let rect = layer.selectAll('rect').data(d => d.values)
    rect
      .enter()
      .append('rect')
      .attr('x', d => x(d.x))
      .attr('y', height)
      .attr('width', x.rangeBand())
      .attr('height', 0)
      .on('mouseover', d => {
        clearTimeout(this.unsetHoverTimeout)
        this.setHover(d.x)
      })
      .on('mouseout', d => {
        this.unsetHoverTimeout = setTimeout(
          () => this.setHover(null),
          200
        )
      })
  }
}

Handle styling updates in a parent component

If you decide to go with dynamic styling for your charts — for example by reducing the opacity of non-hovered values, or by letting users change colors dynamically — you should certainly not go through a D3 update to do so. Instead, add a CSS class to your SVG elements that includes a key to the data and/or group they represent, and then handle styling outside of D3 land using your favourite CSS-in-JS tool. I personally am a huge fan of styled-components.

Going further with this idea, if you are building a dashboard or anything that gets you maintaining multiple charts in your codebase, you might want to share the parts of the state that dictate your charts styling into a parent component — I love Redux for state management, but pick anything that works for you. You can then apply styling on that parent component, and it will be shared by all chart components in its subtree. For example, in my playground dashboard, none of the chart components need rendering when the user picks a new color from the pallet, it’s all handled by rendering the dashboard component. Similarly hovering the barchart does not re-render the scatterplot although it looks like it does; the dashboard takes care of setting the opacity on filtered data. This also has the advantage that you code your styling once and it is handled for all your chart components, so you have one less thing to manage in your charts code.

Use pure components, immutability and memoized selectors

This is not really specific to React+D3, but since I’m on performance tricks, I might as well mention it. You can make big wins in render performance by reducing the need for React to even render your components (recompute the virtual DOM) and perform the reconciliation when you know there is nothing to update. There are a few techniques that you should employ together to do this.

  • React components normally update when their parent component does or if their props or state change. You can extend React.PureComponent instead of React.Component and your component will only update if the shallow comparison of its state and props shows differences. See the docs for details.
  • Because deep comparison can be expensive in Javascript, especially with visualizing large datasets, pure components only perform a shallow comparison. This means your component’s state and props are compared by reference to their previous self. In order to use pure components effectively, you should be sure to make your state and props immutable. One option to do this is the awesome immutable.js which, being a Redux user, I simply apply on my entire Redux store at initialization. I then make sure to apply immutable modifications to the store in my reducers.
  • Props are passed down from parent components or containers, they are often calculated by these components from the state. You need to make sure that new values are not recomputed when the state has not changed. To do so, you can use memoized selectors with reselect, a “selector” library for Redux. Reselect only computes new props values when the underlying state has changed, and return the reference to the previous value if the state has not changed, making it a perfect fit for pure components and immutable redux stores.

That’s all folks!

It has been an exciting challenge trying to get the best of React and D3 in a seamless developer experience while keeping performance in mind. A great thank you to the authors of the articles I posted above for getting much of the problem stated and for providing some great answers. A huge shout out to Oliver Caldwell for masterminding the react-faux-dom approach. I hope to see it evolve, improve further, and get the community attention that I think it deserves. I leave the rest to the comments section. We can hopefully see some interesting ideas and debate about this these techniques.

Edit: a recent article by Marcos Iglesias is a great addition to this with a look at more charting libs for React and D3, it’s at https://www.smashingmagazine.com/2018/02/react-d3-ecosystem.

Top comments (0)