Using D3 With React

HG King
7 min readNov 20, 2021

Low Level SVG Control with High Level React Lifecycles

Draw funky vectors like this… then animate them using basic React!?

HG King — November 2021

Our Goal: To be able to render and control D3 vectors inside of a React component.

Things you will need:

React likes to use JSX, which is great in most cases. One of those is when you are explicitly not trying to use JSX, like with D3. D3 prefers to use selectors and data. D3 does not belong anywhere inside of a render function. This begs the questions, how do you use D3 inside of React? Well, let us work through this problem by creating 3 files.

To start, you will need to install D3 into your project. This is as easy as npm install d3. From there, we will build ourselves a vector. Just to set the scene, this is what will be controlling the actual drawing. We will abide by the “separation of concerns” rule and designate our vector to draw, update, and resize itself. Now let’s see it:

Vector

// save in vectors/vector.js
import * as d3 from "d3"
class Vector {
constructor(containerEl, props) {
this.containerEl = containerEl
this.props = props
const { width, height } = props
this.svg = d3.select(containerEl)
.append('svg')
.attr('width', width)
.attr('height', height)
this.svg.selectAll('path.lines') // you can draw other paths if you use classes
.data([0])
.enter()
.append("path")
.attr("class", "lines") // classes help you be specific and add more drawings
.attr("fill", "none")
.attr("stroke", "#000000")
.attr("stroke-width", "0.5")
this.update()
}
getDrawer() {
const { count, amplitude, offset, multiplierX, multiplierY, width, height } = this.props
const originX = (width/2)
const originY = (height/2)
const arc = Array.from({ length: count }, (_, i) =>; [
originX + (amplitude * Math.sin(multiplierX * (i - offset))),
originY + (amplitude * Math.cos(multiplierY * (i - offset)))
])
return d3.line()(arc)
}
update() {
const { svg } = this
const drawer = this.getDrawer()
this.svg.selectAll('path.lines').attr("d", drawer)
}
}
export default Vector

Okay, we have a class called `Vector` which has three methods defined inside. If you are familiar with Object Oriented Programming then none of this should be a surprise. For those who are not, we want to keep our vector as self contained as possible. If we want to mutate any of our properties within, we should define a set___ method that does the assigning to this.___ . Now let’s take a look at what is inside.

First is our constructor. It takes in a container element as well as our props. We save both of those into instance variables to use later. We then select the container element and append an svg to work in with our height and width bounds. Our props are the main way to get information into the vector.

The next function is the actual drawing. It is best in my opinion to keep all of the drawing in this one method if possible. We use the svg to bring in some lines. Again, what goes in here is pertinent to whatever you want to draw; lines, circles, curves, whatever. You could get away with just these first two functions, construct and update.

The last function, getDrawer , is simply returning a D3 line for us to draw. This is specific to our vector and will change based on whatever you want to draw. Here, we are making an array of plot points that D3 will connect into one line. We use simple harmonic motion to calculate data points to draw funky shapes. After this article, please do play around with the multipliers and see what happens.

Animator

Now we need something that is in charge of our vector. The challenge, as mentioned a priori, is to bring our vector into the React render function. We also want to do some abstracting if possible. Two events that we want to hide details on will be resizing and timers. These we can tuck away into a component, but we will leave that until later. Our solution is a component that I call an Animator. It will be a place where we put our repeated code, such as instantiating and cleaning up. Our goal would be to pass in a handful of variables into our Animator from the parent and then call it a day.

Let’s look at some code:

// components/animator.jsx
import React, { useEffect, useRef } from "react"
export default function Animator( props ) {
const { vector, options, setVector, ...other } = props
const refElement = useRef(null) const initVector = () => {
const v = new vector(refElement.current, options)
setVector(v)
}
useEffect(initVector, []) return (
<div ref={refElement} {...other} />;
)
}

Okay, here we have our first React component. It is succinct but will do a bit of heavy lifting for us. Like usual for these components, we have props that are brought in from the parent. We are also using a ref. This could very well be new for some readers so let us go over it quickly.

A ref is essentially how React lets you put HTML elements in variables. Here we pass the ref into the div at the bottom. This specifies what element we are referring to, and now we can use refElement.currentto refer to it. If you recall the previous section, we accepted the containerEl as a constructor argument. This is how we generate that element and are able to use D3 on it.

The other main property of this component that is doing a bit of heavy lifting is the initVis section. We use a side effect here, denoted by useEffect, to run some function only at the very beginning of this component’s lifecycle (denoted by the empty array as an argument). The function that we use will instantiate our vector and bring it into existence. Also, it connects the vector to the parent container, look out for this in the next section. Now at this point, all of the hard stuff is taken care of!

Parent Component

Last but not least, we will have a self contained parent container that will pass our vector and the properties into the Animator, which will render our sweet drawing! There is not much to say about this one so lets look at the code:

// containers/vector.jsx
import React from "react"
import vector from "../vectors/vector.js"
import Animator from "../components/Animator.jsx"
let _vector
const setVector = (v) => { _vector = v }
export default function Vector( props ) { const multiplierX = 3
const multiplierY = 1
const options = {
count: 1000,
height: 700,
width: 1300,
amplitude: 300,
length: 5,
offset: 0,
// play around with these numbers
multiplierX,
multiplierY,
}
return (
<Animator
vector={vector}
setVector={setVector}
options={options} />
)
}

So… if you understand React, then this will be pretty straight forward. The only thing to mention is the `vector` related code sitting at the top of the file. This is a little bit of trickery, but is a clever way to have a reference to our vector. This way we can do all sorts of things like update and set fields as we please inside of this parent container. Will come in handy when you want to add buttons and other forms of inputs.

Putting It All Together

If you were to copy and paste all of the above code snippets into the correct files, name them as the comments at the top specify, and then render the parent component somewhere in your project, you should see a surprise!

Go Deeper

Okay, now that we have a drawing on the screen, there are some ways that you could take this further. I will provide the reader with two challenges and recommended approaches to extend the features of this pattern.

Component Level Inputs

Our drawing is pretty neat, don’t get me wrong. But, a static drawing will only get us so far. We are not so easily amused. What makes these drawing cooler is when your users can alter them and see things change. Let’s add some inputs for this case. To do that, try and follow these steps:

  • Inside of our D3 vector, let’s add setMultiplierX and setMultiplierY methods. They will just mutate the props like this: this.props.multiplier_ = multiplier_. Also call the update method (this is important!)
  • Add to our parent component two state variables for our x and y multipliers, replacing the declarations that are already there.
  • Also add two number inputs for our x and y multiplier into the HTML section of our parent component. It will look like: <input type=”number” value={multiplierY} onChange={setMultiplierYHandler} />.
  • Finally, we will write the multiplier handlers. They should call the vector’s setMultiplier_ method and update the state variable.
  • Try it out and hopefully it works!

Timers

Now maybe we want to animate these drawings. Possibly set a frame-rate and update the vector each frame. This can be done with few small steps. I will list them here:

  • Decide what you want to have change on your vector for each frame. We will use the offset property in our case, since it is playing the roll of the time variable t in our simple harmonic motion functions.
  • Add a setOffset method to our D3 vector. It will simply take a value and do this.props.offset = offset and then call the vector’s update function.
  • Add an offset state variable to our parent component, as well as a time = 10 variable which will be our framerate, and step = 1 which will be our delta.
  • We need something to call this function, so let us introduce an intervalHandler function to our parent component. This will set a new offset (offset + step) using both the state variable and the vector’s setFunction method.
  • Pass intervalHandler and time into the Animator as props.
  • Read the following code thoroughly, then paste it into the Animator:
const setupTimer = () => {
if ( !intervalCallback ) { return }
const interval = setInterval(intervalCallback, time)
return () => clearInterval(interval)
}
useEffect(setupTimer, [])

--

--