React Hooks and D3
By: Karol Stopyra
Tags: React, Data Visualization, JavaScript, SVG, D3js

TL;DR

Use React as container for D3 which manage SVG and everything inside it, application level management should be done by React. See code listing at the bottom of article for minimum viable example.

The approach

There has been already a lot of articles written about how world of React and D3 can(‘t) work together. Personally the best overview I found was made by Shirley Wu in this presentation and by Thibaut Tiberghien in this article. I encourage you to take a look into these articles if you want to know more background on the issue.

Nowadays, with React hooks things get simpler but we need to tweak our approach a bit, so let’s start with setting new mindset of how to make React and D3 work together.

Our approach is to set clear border between “React world” and “D3 world”. This border line is an SVG (or Canvas) element, a root of all d3 visualisations. Root element and anything inside it belongs to D3 world, everything above it belongs to React world.

Advantages of such solution are:

we don’t mix D3 and React logic inside one React component, cleaner code = happy dev.

we can utilise full power of D3 transitions and D3 update pattern,

we can utilise full power of React state management and can wire necessary external features (like fetch calls or other React components’ interactions),

we can reuse D3 component for non-react projects as it is independent module.

React component

React component renders SVG Container (usually a div element) which is passed down to D3 world with useRef hook. React will not care about what’s inside SVG and about any of its changes, until triggered explicitly by D3 component call. React just gives a container for an SVG and says ‘Hey D3, do your magic here and let me know if you need anything’.

Obviously the same React component can also render additional controls (like drop-downs and buttons) for our visualisation but to keep our example simple we will omit this part for now.

Initial setup of empty React component looks as follows:

We return only <div> as container of our visualisation and keep ref for further use.

D3 Component

Next, we have D3 component. This is our visualisation, minimal setup can be just a function (but will rather have bunch of functions) like:

D3 component minimal setup

This will work nicely but for more complex visualisation we probably would like to layout our functions better — wrap them in a class, like so:

D3 component as a class

Couple of useful hints on how to layout more complex D3 components can be found in article of Amit Patel “Making of: Line drawing tutorial”. It contains examples of building classes for D3 components and how to marry D3 functional approach with OOP principles.

Wiring components together

With above D3 component we can go back to React world now.

What we need here is to provide to our D3 component everything it needs to render SVG. Usually these are at least: container element reference we already have (via useRef hook), data, width and height for visualisation (via useState hook).

To wire all this state we will use useEffect hooks. Take a look at code below:

React component with hooks that wire d3 component.

For every action we build new useEffect hook. We will extract them as separate functions so the code will be more clear to read.

Now, most likely we want to establish two way connection between D3 and React.

React to D3 communication

Like handling resize event. We will create another useEffect hook which listens to window resize event and updates width and height state of React component (I’ve added some debouncing so updates are not too frequent).

useEffect(() => { let resizeTimer;

const handleResize = () => {

clearTimeout(resizeTimer);

resizeTimer = setTimeout(function() {

setWidth(window.innerWidth);

setHeight(window.innerHeight);

}, 300);

}; window.addEventListener(‘resize’, handleResize); return () => {

window.removeEventListener(‘resize’, handleResize);

}; }, []);

Notice that we use return statement to remove event listener when component will unmount.

Next, to make D3 component responsive to React state updates, we just need to call appropriate method on every width or height state change:

useEffect(() => { vis && vis.resize(width, height); }, [ width, height ]);

We use vis && prior to calling vis.resize because resize event can occur before vis is created (data being loaded).

D3 to React communication

Let’s say our D3 visualisation has clickable data points which trigger some view update in React world.

Inside our D3 component we probably have something like:

d3.selectAll(‘.datapoint’)

.on(‘mouseup’, d => this.setActiveDatapoint(d));

where setActiveDatapoint is an arrow function:

setActiveDatapoint = (d) => {

// some internal code here

this.props.onDatapointClick(d);

}

Why arrow function? Arrow functions inside class are auto-bound to instance of that class. It’s important as we use this keyword in mouseup callback.

If we would use normal class method here, then this in this.setActiveDatapoint(d) would refer to .datapoint selection not to our vis object.

Alternative way of making it work without arrow functions (which may be less performant than class methods) would be to use class method and to bind it in constructor with (most likely familiar) expression:

this.setActiveDataPoint = this.setActiveDataPoint.bind(this);

As we will probably have only one instance of D3 component I think using arrows here is not a problem.

Last thing we need to do is to pass appropriate props to D3 component in React world. See below code final version, the onDatapointClick prop is included in initVis function of React component.

Code Listing

Below is the code for both D3 and React components. As mentioned, for React component I extracted functions from useEffect hooks.

Disclaimer

At the moment of writing this article React hooks are an experimental proposal, please refer to the official documentation any changes that might have occurred.

Thank you for reading!