RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

D3 and React Faux DOM

Author’s Note: This post makes my original post exploring React + D3 obselete. I strongly recommend react-faux-dom (on Github) over my previous post’s suggestion.


TL;DR, Hear it straight from the lib author: Oliver Caldwell wrote this blog post about react-faux-dom, which enables a cleanly organized and powerful combination of React and D3.

That post in four bullet points:

  • D3 works by mutating the DOM. Select a DOM element, append children, etc.
  • React works by reconciling the DOM. Build a tree, compare to DOM, determine which elements to add/remove/change.
  • DOM mutation (like D3 does) and DOM reconciliation (like React does) don’t work together so well.
  • react-faux-dom makes a fake DOM to support D3. It might seem silly, but it enables us to support D3 while remaining within React.

(Note: regarding the second bullet, this post from the React docs is worth a reread.)

Using a fake DOM means we can drop D3 scripts into a React component’s render() function and it’ll just work. It was trivial to prove out in a production PR:

(sparkline.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// inspired by: https://github.com/QubitProducts/d3-react-sparkline/blob/master/src/d3-react-sparkline.js
import d3 from 'd3'
import React from 'react'
import ReactFauxDOM from 'react-faux-dom'

export const Sparkline = React.createClass({
  propTypes: {
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    data: React.PropTypes.array,
    interpolation: React.PropTypes.oneOfType([
      React.PropTypes.string,
      React.PropTypes.function
    ])
  },

  render () {
    const {width, height, data, interpolation, max} = this.props

    const el = d3.select(ReactFauxDOM.createElement('g'))
      .attr(this.props)
      .attr('data', null)

    const x = d3.scale.linear()
      .range([0, width])
      .domain(d3.extent(data, (d, i) => i))

    const y = d3.scale.linear()
      .range([height, 0])
      .domain([0, max])

    const line = d3.svg.line()
      .x((d, i) => x(i))
      .y((d) => y(d))
      .interpolate(interpolation)

    el.append('path')
      .datum(data)
      .attr({
        key: 'sparkline',
        className: 'sparkline',
        d: line
      })

    return el.node().toReact()
  }
})

Rendering a sparkline is as simple as <Sparkline width={500} height={500} max={10} data={[1, 3, 2, 5, 4]} interpolation={"basis"} />. We get the benefits of React semantics AND the D3 API, both neatly organized in their respective places.

I consider it a clear win to maintain React component organization while leveraging the power of all that D3 offers, but I suppose what it comes down to is this:

So many code design decisions boil down to the border between things. The interface. The “line” between where React component code belongs and where D3 code belongs. Ultimately, this still leaves us to fill in the lines with whatever we choose to write, but this library’s placement of the “line” is an improvement over anything else I’ve seen.

As the author writes, “All [React and D3] concepts remain the same, react-faux-dom is just the glue in the middle.” This clean separation is hugely helpful in writing dataviz React components with D3.