Stateful Peanut Butter and SVG-elly: A React + D3 Tutorial

Leigh Steiner
codeburst
Published in
9 min readJul 3, 2018

--

React and D3, living in harmony

AUTHOR’S NOTE: Hey guys! this article is deprecated, because it isn’t up to date with the most recent stable version of React, and encourages you to do some things that are not, now, the best way to do them. Check out the updated version, with all new content on web accessibility and React Hooks, right here!

React is, right at this moment, taking over front end development in a way that, to me at least, is pretty exciting. Tomorrow, that may not be true, but the number of codebases that rely heavily on React is large and growing. If you’re here, reading this, you probably don’t need me to tell you any more about that. And then there’s D3. There are fewer people and projects using D3 than React, but it’s hard to argue this point: when it comes to creating data visualization in 2018, there really isn’t anything that comes close, whether we’re talking about the sheer depth and power of the library (have you seen what D3 can do with string interpolation and scales?), or the immense quantity and quality of community support and information that exists.

Both React and D3 give a programmer rich resources for DOM querying and manipulation that leave alternatives like jQuery and even native JavaScript pretty well in the dust. The problem? They do so in totally different ways, which means that trying to use both at the same time isn’t as straightforward as one might like it to be. Both libraries try to update the DOM and, if not managed properly, can conflict with one another and prevent both from giving you their expected output. Which, let me tell you from is experience, is irksome when you really would like to use both at once — if you were say, building a feature where you wanted to create SVG graphics based on user inputs (which is what we’re gonna do in a second).

Never fear! What follows is a quick and simple tutorial that will have React and D3 braiding each other’s hair and going splits on a rich dessert in no time.

(If you love-love-love spoilers, you can check out the end result of this tutorial on its feet here: https://d3-react-tutorial.herokuapp.com/, or you can follow along in the code base here: https://github.com/LeighSteiner/d3-react-tutorial.)

The Set Up

The idea here is not to build a full-scale application, but to create a modular set of practices that you can port into any project with React + D3, so we’re going to keep things as simple as possible, and that means…create-react-app! Go ahead and create-react-app <your-app-name> in your terminal (don’t forget to install it globally if you haven’t already), npm install --save d3, and meet me back here.

All set? Awesome. Your file structure should look a little something like this:

The Magic

So, the goal here is to prevent React’s virtual-DOM shenanigans from interrupting D3’s more directly surgical approach. And the simplest way to do that is to just literally separate them. (I told you this was going to be easy. Didn’t I? Well, this is going to be easy) I said before that we wanted to build a feature where our application would draw SVG graphics based on some kind of user input. The first thing to do is think about what the responsibilities of React are going to be, and what we want D3 to handle. We know that React is pretty slick when it comes to forms and controlling and (temporarily) storing user input, and that D3 is the hot shot when we’re talking about SVG’s, especially SVG’s that are dynamically based on data. Sounds like we have a nice, neat division of labor, right?

A good principle generally in application building is to let each component do one thing, so that it can do it both well and thoroughly, so let’s make a component for our controller tasks (Controller.js), where we’ll let React run the show, and our drawing tasks, where D3'll call the shots (Viz.js), both in that same src folder. In Viz, even though we’ll be exporting a React component, we’re going to stop React from doing most of the things we usually want it to do, while over in Controller, even though we’re going to hold all the information about what we want D3 to draw, we’re not even going to import the D3 library into the file.

Controller.js

This is our aptly named file that houses the form that takes in our user inputs. Under the hood, it’s also going to keep track of every shape the user creates and wants to draw, in its state. If you’re reasonably familiar with React already, this is 100% straightforward. We just write up the form with whatever information we need from the user (in this case, the color and size and each circle the user requests), along with the appropriate change handling functions to store the current shape in the React component’s state. Your code might at this point look a little something like this:

A nice, tidy React form

The fun gets started when a user has made their most-auspicious selections, and decides they’re ready to go ahead and hit that draw button. Our React state isn’t only going to keep track of the selections the user is in the process of making, but it’s also going to keep a running tally of previous selections, so that we can draw as many circles as we would like. So, when a user submits their selection, we’re going to grab the chosen color and width, and add them to the toDraw list. And that list is going to be passed down to our D3-eriffic Viz as props, which will handle everything from here on out. Just to keep you up to speed, here is what our Controller.js file should look like when we’re done with it:

Ready to send all that tricky data visualization work off to D3, where it belongs

Ta-Da!

Viz.js

We’re going to keep Viz as simple as possible, as far as React goes. We want React to put up its feet and have a drink — in fact, all we want in our render method is an empty div with some snappy id to reference and hang things on when we start our drawing.

I know I keep saying this, but here’s where it gets cool. I bet you already know, roughly, how React updates the DOM so quickly and cheaply, right? Essentially (and extremely briefly), whenever there’s a change to a component’s state or props, React compares its model of how the DOM should look (that is, the Virtual DOM) and how it really is, and only spruces up the elements that have actually changed. What you might not have known is that React comes with a built in switch that we can flick to tell it to knock all that fancy shit off. I’m talking about the lifecycle hook shouldComponentUpdate. (You can learn more about it here. Note: The React docs warn that future versions of React may not implement this perfectly, and suggest using a PureComponent instead of a standard one if you need to control updates. However, the relevant code we’re looking at will remain almost exactly the same in that case, so I’ve gone ahead and kept everything in basic class components.)

So, for the Viz component, we want React to do as little as possible. We don’t want React to update the DOM at all, because we’re letting D3 do all of the updating in this component. Our handy new lifecycle hook will be an almost-empty function that simply returns false in all cases. Like so:

shouldComponentUpdate() {
return false;
}

That clears React out of the way. But where do we let D3 do its thing? Logically enough, we want to draw our initial SVG and children in the componentDidMount lifecycle hook (You could just as easily use componentWillMount in this case, but I use the former so that nothing is blocked that doesn’t need to be, if say, we want to render our SVG based on data that might arrive asynchronously, for instance). If you have a very simple feature in mind, you might be able to stop right there — draw once, and you’re done. But probably, if you’re talking to the user anyway, as we are here, we want to keep talking to them. We don’t just want our user to be able to add one circle, or to have to wait until they’ve decided how many circles they want total before we draw them — we want the user to be able to see their graphic update as they add to it. So what we’re going to do is pull out the D3 code we’ve written into its own function, that simply takes the component props as an argument. Here’s what mine looks like:

draw(props) {
const w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
d3.select('.viz').append('svg')
.attr('height', h)
.attr('width', w)
.attr('id', 'svg-viz')
const bubbles = props.shapes
let min = bubbles[0].number
let max = bubbles[0].number
for (let i = 1 ; i < bubbles.length; i ++){
if (bubbles[i].number > max) {
max = bubbles[i].number
}
if(bubbles[i].number < min) {
min = bubbles[i].number
}
}
const radiusScale = d3.scaleSqrt().domain([0, max]).range([0, max])
const simulation = d3.forceSimulation()
.force('x', d3.forceX(w/3).strength(0.05))
.force('y', d3.forceY(h/3).strength(0.05))
.force('charge', d3.forceManyBody().strength(-1300))
.force('collide', d3.forceCollide(d => radiusScale(d.number)+1))
const circles = d3.select('#svg-viz').selectAll('circle')
.data(props.shapes)
.enter()
.append('svg:circle')
.attr('r', d => d.width/2+"px")
.style('fill', (d) => d.color ? d.color : 'purple')
simulation.nodes(props.shapes)
.on('tick', ticked)

function ticked() {
circles
.attr('cx', d => d.x)
.attr('cy', d => d.y)
}
}

As you can see, the d3 grabs that single <div className="viz"/> that the render method returns, and uses it to hang an SVG on, where we’ll draw out all the circles the user has sent us. What’s nice about doing it this way is that not only are all of our SVG elements generated in this function, but all of the styles that apply to them are right here too — we don’t have to go hunting through our CSS files to figure out why things look the way they look, and if we later want to export the SVG file we create (but hey, that’s another blog post), we won’t have to do any fancy file-reading backflips to make sure that we’ve got everything we need.

Done and dusted, right? Not quite! Like I said, we’re first going to call this in our componentDidMount function, where we will pass it this.props as its sole argument. But what about when our user adds a new circle? We’ve already told the component not to update!

componentWillReceiveProps comes very handily to our rescue. This hook takes the incoming new props as its parameter, which we can compare to the existing props to decide if there’s a change that matters to us (for instance, the length of our new shapes array is different from our old one), and update accordingly. (Note: When React 17 comes out, this method will be deprecated, and can be replaced with getDerivedStateFromProps — as above, however, the principle remains the same in either version.) In the case of our little drawing app, if we’ve received new bubbles, then we want to redraw our SVG with the updated list. So, in this case our hook might look something like this:

componentWillReceiveProps(nextProps) {
if(this.props.shapes.length !== nextProps.shapes.length){
d3.select('.viz > *').remove();
this.draw(nextProps)
}
}

What next, you ask? Well, we have our controller component organizing and sending our data to be visualized to our d3-viz component, which draws our SVG, updates it appropriately, and cleans up the mess when it’s done.

You guys, I think we did it.

Obviously, this is kind of a silly application. But I wanted to keep it as little and simple as possible, so that it would be clear how simple this really is, and how portable to more complex business-logic needs, and even to other sorts of third party libraries, the principles laid out here really are. At the end of the day, all we’re really doing is following good programming practice — allotting out responsibilities, one per module, and keeping everything else out.

(If you want a little more guidance about how I did it, this is a reminder to check out the codebase at https://github.com/LeighSteiner/d3-react-tutorial, play with the app at https://d3-react-tutorial.herokuapp.com/, and to ping me with questions, ideas, or how you like to make your data viz pop.)

✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.

--

--

Full Stack Developer. Currently telling stories with data @ Locus Analytics. Former Teaching Fellow @ Grace Hopper. Bug me to blog about D3. pronouns: she/they