D3 within React the right way

If you follow me on twitter (I’m @OliverCaldwell, if you don’t) you will have noticed that I’ve been tweeting about D3 and React a lot recently. More specifically, how to use both together in a pleasant yet efficient way. After a few weeks of thought and a couple of potential solutions actually being built and used, I settled on what I think is the ideal way to use DOM mutating JavaScript (like D3) from within React.

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, my solution allows you to even use React developer tools with the SVG generated by D3.

TL;DR (although I’d quite like it if you read the rest!): Use react-faux-dom to seamlessly blend D3 and other libraries into your React component tree.

The problem

D3 (data driven documents), as you probably know, is a JavaScript library that helps you build visualisations or anything else for that matter. It’s actually very general purpose since you can render SVG or regular DOM elements with it.

It works by mutating the DOM element provided to it, usually a root node you placed into your HTML. You call .append('p') and it inserts a <p></p> as a child of the root node you selected.

React on the other hand has you build your application through one big call stack to various render methods. You create a tree of objects that represents your application and its state then React works out what DOM elements it should mutate, add or remove on your behalf. This process is called reconciliation.

If you give an element React created to D3 and say “hey, D3, set the width to 100, thanks” it’ll happily oblige. Then React will notice and get pretty upset that you went behind its back and messed with its perfectly reconciled DOM. This isn’t great, as you can imagine.

So, the way we usually get around these problems is to tell React that from this component and below, it shouldn’t interfere, we will manage the DOM below this component manually. That’s where things like react-d3-wrap come in, they define a component you can inherit from that pulls your code out of the React tree and lets D3 do its thing, React skips happily past this part of the DOM.

But what if we want to keep the React tree and use D3? What if we don’t want to have part of our DOM not managed by our benevolent DOM reconciling God?

First (deprecated) attempt: d3-react

d3-react is essentially a plugin for D3 (injected into the D3 prototype) that gives you React methods within the D3 API. What this means is you need to use .prop() instead of .attr() and then you can call .toReact() when you’re done and return that result from your render function.

You create a DOM node and pass it to D3 on each render. It mutates this detached DOM and then gets converted to React elements when you’re done.

This has multiple problems, first and foremost: You can’t use the full D3 API or existing components! You have to use .prop, you can’t use .classed or .style for example. This is just unacceptable, any good solution needs to also work with existing components or with minimal tweaking, not a full rewrite.

The other problem (or one of them, anyway) is that you’re building and mutating a full real DOM tree on every render and then throwing it away. This is pretty inefficient, the DOM isn’t exactly light weight, that’s why we’re using React in the first place! (well, one of the reasons)

It works, you don’t need to opt out of the React render tree, but it’s not great. So what’s the next step after this?

We make our own DOM

Yeah. I thought it was a stupid idea when it first came to me, but the more I thought about it the more it made sense. A fake DOM that supported enough methods to work with D3, but no more. If it needs to work with more libraries in the future you just need to add the missing DOM methods, easy.

And so, react-faux-dom was born. A fake DOM with enough methods to trick D3, including a selector engine and partial support for addEventListener etc. (addEventListener sets the appropriate prop value, so if you add two, it will overwrite the first, I may improve this later)

You can use the full D3 API (no special React methods), inspect it with React developer tools, have efficient D3 components without carefully placed .enter() / .exit() calls, use React animation techniques instead of D3s (good and bad, both approaches work well) as well as render on the server side. Yeah, buzzword time, this will allow you to have isomorphic charts.

drops mic

I copied one of mbostock’s charts (the awesome creator of D3) and rendered it through react-faux-dom easily. Nothing needed to change, it just works. I’ve also converted a complex chart over to this at work from react-d3-wrap, it was pretty easy to do and now it’s far more efficient. That chart even has dynamic resizing and hover tooltip interaction!

A side effect of migrating that chart over was that I could make use of this.setState({...}) to re-render my chart upon mouse interaction or window resize. It’s now much cleaner and easier to follow in my opinion.

The best thing I can compare this to is a lightweight and simple version of jsdom that targets React elements and only supports the minimum required DOM API to work with D3 (right now).

d3-react-sparkline is a small React component I built at work (I work at Qubit, it’s awesome here) originally using my first approach but migrated to faux DOM. It serves as a good example, it should be familiar to React users and D3 users alike. All concepts remain the same, react-faux-dom is just the glue in the middle.

Other implications

Firstly, ISOMORPHIC CHARTS! A concept I find so awesome I thought it was worth mentioning again, in bold all caps. No more “we’re just fetching the data for this chart, brb”, send that rendered SVG straight down to the browser and then have React pick up where it left off on the server with the data as it changes over time. Amazing!

It’s not just charts though, I see a lot of function calls happening in amongst JSX to turn complex data into complex elements. Those elements don’t have to be bars in a chart, it could just be a <ul> with other nested <ul> tags inside. These function calls can be hard to follow when compared to D3’s declarative chaining syntax.

Why not use D3 as your JSX? D3 is an excellent (if not the best) tool for turning data into DOM.

This results in a list containing each value passed through props, each with a unique key automatically assigned by index if you didn’t provide one. This automatic key assignment allows you to use existing D3 components without getting warnings from React, you can assign your own keys though if you feel it’ll optimise the reconciliation.

We’re taking the D3 data driven mindset, but running it within the stateless simple world of React seamlessly, no DOM required.

I’m using this in production right now, this is not just a little script knocked together in a day (although it was), it’s built to solve an existing problem in a nice way. Let me know if you find any any issues with it, raise a bug, even fix it if you can. It’s ready to use on real projects.

I hope many of you find this post and project useful, I’ve tried to explain “why” and not just “what” it is. Please feel free to give me your feedback, I’m very interested in hearing what others working on similar problems think about this.

Thanks for reading.

  • Thomas

    Hi,

    nice work! What do you think about this approach:

    https://github.com/esbullington/react-d3

    • Thanks! I’ve looked at that a few times, if those charts fit your need, they’re great! I can’t use any existing chart systems because I’m building them to a very specific design.

      As far as the implementation goes, it’s nice, this is a good example https://github.com/esbullington/react-d3/blob/master/src/common/Voronoi.jsx#L37-L59

      My only problem with it is that it’s very manual. It’s using D3 for the math (which it’s amazing at) and manually mapping that to React / JSX elements. It’s an equivalent approach.

      (bias warning!) I personally prefer my approach because it doesn’t break backwards compatibility with any existing D3 code. It’s a way to use existing D3 tooling but render it efficiently through React with the React ethos.

      So, I like it, but I prefer to use the D3 API to work with D3, then at the fringes of that API integrate it into the React workflow.

  • Ricky Duckworth

    Wish I’d had thought about the fake dom idea! Would’ve saved me so much time – great work.

  • Dinosäur Jay

    thanks Oliver for the great work! But unfortunately, I could not manage to migrate my force directed graph to a faux dom version. Could you post a really basic force directed example such that I can track down the problem? Somehow, I cannot really set the “transform” attribute in the tick function. Thanks in advance!

    • Yeah, that won’t work out of the box since D3 sets up all sorts of listeners and timers to mutate the DOM. Give me a few days (maybe over the weekend? It might take me a little while but I’ll get around to it) and I’ll see if I can get a nice example force directed graph working. Will probably require a little tweak to the library too.

      This work could lead on to allowing D3 to animate stuff instead of React too. I’ll get back to you. It wasn’t in scope when I initially built this, I just didn’t have a need for animations etc. I realise that others do though.

      Here’s an interesting issue on animations (not force, but still): https://github.com/Olical/react-faux-dom/issues/2

      • Dinosäur Jay

        ah, Good luck with that. I’m working on a rich graph visualization with a wide range of user interaction and it is really a pain to keep all the styles of the nodes in sync. An easy call to re-render would be awesome!

  • nascif

    Great job! I always had the feeling that the D3/React integration efforts were a bit too hacky but this one just makes sense – use the best of both worlds.
    +1 for supporting transitions and animations, they are a huge part of what makes D3 so powerful.

  • Matt Fairbrass

    Excellent work Oli, I really liked your end solution and the accompanying explanation of this blog post to go along with it. As noted by @dinosurjay:disqus it would be really good if you can add support for animations as this is a large use case and one of the primary reasons for using D3, so that we can create rich user experiences in data.

    Keep up the good work, I’ll be keeping an eye on the project for future use.

    • Thanks a lot, Matt! Maybe Maybe it’ll come in handy for you guys some day. This post has been one of my most popular so far, I’m so glad it’s been well received. It’s certainly saved us some headaches here.

  • WM

    Hey, you should add this intro article link in the README.md of repo.

    • Hey. It’s already in the repo description, but why not. Will add it at some point.

      • WM

        Didn’t notice that. Thanks!

  • Edgar Pabon

    I’m trying to get the faux dom to work with c3 but I haven’t had any luck getting it to render. I’ve tried to create a fake node first and then bind the c3 chart to that node, and then render the final product in react. The plain d3 works just fine. Has anyone been successful in making this module work with c3?? If so, how?

  • Roger

    finding a major problem.

    if I have an es6 react component, and I do:

    svgContainer.selectAll(‘g.pos’).each( (d,i) => {
    console.debug(this);
    debugger;
    });

    when the debugger stops in chrome.. “this” is a ‘g’ node. (correct).
    But console.debug(this) prints out the react component
    (and this.attr(‘etc’) fails because the React component doesn’t have an attr method).

    Huh?!?!?!

    • spoj

      Don’t use arrow functions in this case, since you don’t want to lexically bind this here.

      • Thanks for this. It’s definitely an arrow function thing, not a faux DOM thing 🙂

  • CalebEverett

    Hi, thanks for creating this and writing it up. I used it to mock up an interactive bar chart and table.

    http://si-app.com.s3-website-us-west-1.amazonaws.com/

    It is definitely hackish owing to my lack of programming experience, but functionally works. I’m loading in a 2,300 record csv file with 256 fields and then doing some aggregation to get the chart data. Then when you click on the bar, the records are filtered before being displayed in the table which seems to be slowing things down. The other thing that seems to be slowing things down is the tool tip which displays the bar value when you hover over it. Do you have any suggestions on how to speed things up? Thanks again.

    https://github.com/CalebEverett/si-app/blob/master/src/views/HomeView.js
    https://github.com/CalebEverett/si-app/blob/master/src/components/Chart/Chart.js
    https://github.com/CalebEverett/si-app/blob/master/src/components/Table/Table.js

    • Hey, I hope you’re finding it useful! Good to see React + Redux in use too 🙂

      At a first guess I’d say that you should separate some nodes out into other React components and add meaningful unique keys to elements to help React work out what to render. This is part of the reconciliation process, you can read all about it here https://facebook.github.io/react/docs/reconciliation.html

      It all looks fine to me, you’ve just got to give React hints as to what has changed. That chart should render extremely quickly, so by breaking it up a little bit and adding keys you should see a boost.

      You can also use JavaScript profiling to work out where the CPU is being used https://developers.google.com/web/tools/chrome-devtools/profile/rendering-tools/js-execution?hl=en

      You may have some hot spots where you’re doing something expensive on every render. Just pay attention to your profiling and you should find whatever is slowing it down. There’s definitely some easy wins in there, react-faux-dom + D3 is definitely fast enough to render this quickly. It’s just normal React performance things you need to focus on.

      Best of luck, and feel free to ask if this hasn’t answered everything!

      • CalebEverett

        Awesome, I was thinking about how to separate things out so the whole chart didn’t have to rerender when only the tooltip is changing – .thanks so much! i’ll track those down and give them a try.

  • tacomanator

    I like the approach you’ve taken here, which works well in simple cases.

    I’m on the fence as to whether it will scale enough for my needs. I thought I would share my experience with others.

    The faux-dom approach does simplify managing the chart. I tested it on a relatively simple chart with two axes, two lines with about 30 points each, area fills, and a voronoi to display tooltips with point values of the lines on hover. I found the faux-dom solution to have noticeable lag between mouse movement and tooltip updates.

    To speed it up I tried moving as much work out of the render method as possible. I stored the scales, axes, line functions, area functions, and anything else that isn’t need in render, in the component state. These values are reset in componentWillReceiveProps() to allow the chart to prop updates. The only thing render does is construct the markup in the faux-dom with the d3 methods and convert it to React with toReact().

    The lag didn’t improve much so I created two versions of the same chart, one with and one without react-faux-dom, and took a profile in Chrome while mousing the mouse around each chart. As can be seen in the profile picture (attached), considerably more work is being done by the react-faux-dom solution.

    The lag is small enough that non-discriminating creators may not care, but it’s noticeable to me. Also, as I said this is a relatively simple chart, so I worry about how react-faux-dom will do on future charts (or more importantly, how much time I might end up spending trying to make it perform).

    Anyway thanks for the alternative approach. I enjoyed exploring it and I hope that it continues to be developed.

    • I’m very sorry for not replying! I didn’t notice the comment, disqus does seem to be a little subtle with it’s notifications sometimes. This is an excellent breakdown, thank you very much. I certainly agree that it’s slower, mainly because it spends more time in D3 / JS land than it does in the DOM. Whereas React expects you to spend little time rendering your UI and it’ll handle the presumed slow part, the DOM.

      You could cache parts of the chart that are expensive to build in the closure of the component / module then reuse them, or keep them as part of component local state. However what this really points towards is something that the faux DOM just isn’t very good at. If you want a large complex and fast chart, I think you should use regular D3 since it’s optimised for mutations.

      ReactFauxDOM is actually very light and should be pretty fast, there’s just a little recursion and looping going on. The D3 rendering should be the slow part. If you want D3 to be fast, you can’t throw away the built chart, you need to mutate it.

      So it comes down to the tradeoff. Efficiency of D3, which you may *need* to make it usable, or stateless immutable components that are easy to reason about. I think if it’s not a perceivable hit on performance, the latter is preferable.

  • Abhisek Jana
  • Aditya

    Hi , Great Article , It worked like a charm 🙂 . I am currently trying to make D3 Responsive in the React page. I tried couple of ways one was to control it in using Aspect Ratio and ViewBox but that ended up omitting some ticks and i was just not able to get it working . Second I tried calling d3.select(window).on(‘resize’, resize). But though it works great in a standalone D3 app. It’s not working inside React. My resize function is something like this: d3.select(window).on(‘resize’, resize);

    function resize(){

    var margin = { top: 30, right: 30, bottom: 40, left:50 };

    var container = d3.select(“node”).node().parent;

    var height = parseInt(d3.select(container).style(“height”)) – margin.top – margin.bottom;

    var width = parseInt(d3.select(container).style(“width”)) – margin.left – margin.right;

    //var width = window.innerWidth – (window.innerWidth*0.5);

    console.log(width);

    xScale.range([0, width]);

    yScale.range([height, 0]);

    lChart.select(“x axis”)

    .attr(“transform”, “translate(0,” + height + “)”)

    .call(hAxis);

    lChart.select(“y axis”)

    .call(vAxis);

    lChart.selectAll(‘.line’)

    .attr(“d”, lineGen);

    return node.toReact();

    }

  • Abhisek Jana
  • Pingback: D3 and React 3 ways | mike.williamson()

  • Alasdair Campbell

    hi, thanks for this post.

    I see errors during sever side rendering in query-selector due to navigator.userAgent being undefined. How exactly is the isomorphic element of this approach supposed to work?

    • Alasdair Campbell

      Beg your pardon, was an issue with no default userAgent set in my build environment. Still learning here.

  • Liran Brimer

    whats the advantages of using this approach over Abhisek Jana’s approach? is it the ability to call “append”?
    because he seem to overcome this with ReactDOM (see his tutorial below).