How to D3 in React 16.3 and beyond

Hi 👋

I’m at MicroConf in Vegas right now, learning how to micropreneur. If you’re here, come say hi, let’s grab a drink. 🥃

Today’s email might seem familiar. It’s a combination of two articles from April. I’m researching how React 16.3 impacts dataviz for an update to my React+D3 book. If you already have it, your update is free. If you don’t, now’s a good time to buy :)

Or at least reply to this email so I know you care.

NOTE: This is a cross-post from my newsletter. I publish each email two weeks after it’s sent. Subscribe to get more content like this earlier right in your inbox! 💌

How to D3 in React 16.3 and beyond

The new React 16.3 brings some changes to the ecosystem that change how we go about integrating React and D3 to build data visualizations.

componentWillReceiveProps, componentWillUpdate and componentWillMount are on their way out. They were great for making React and D3 happy together, but they cause issues with async rendering that the React team is planning for React 17.

You tend to use those now-deprecated lifecycle methods to update D3 objects’ internal state. Things like setting scale domains and ranges, updating complex D3 layouts, setting up transitions, and so on.

But you don’t need to! You can do it all with the new lifecycle API.

Here’s a small example of building a bar chart with React 16.3. Using only approved lifecycle callbacks 😏

You can play with it on CodeSandbox 👇

How it works for charts

The core problem we’re solving is that D3 objects like to keep internal state, and React doesn’t like that. We have to update D3 objects whenever our React component gets relevant new props.

You have to update a scale mapping data to x-axis pixels whenever either data or width change. Traditionally, you would do that in componentWillReceiveProps.

React docs recommend replacing componentWillReceiveProps with componentDidUpdate, but that leads to rendering stale charts. If you update your D3 scales after your component re-renders, that's too late.

Instead, we can move our scales into state and use getDerivedStateFromProps. 🤯

That’s right, you can have complex objects in state now. It’s totally safe.

Here’s the gist of it 👇

D3 in state

Defining D3 objects as component properties used to be best. Now you should do it in your component state.

class BarChart extends React.Component { state = { widthScale: d3 .scaleBand() .domain(d3.range(0, this.props.data.length)) .range([0, this.props.width]), heightScale: d3 .scaleLinear() .domain([0, d3.max(this.props.data)]) .range([0, this.props.height]) };

We define a widthScale and a heightScale. Each has a domain and a range that both depend on this.props. Yes, you can do that in JavaScript class fields syntax.

Update D3 in getDerivedStateFromProps

You then use getDerivedStateFromProps to keep those scales up to date when your component updates.

static getDerivedStateFromProps(nextProps, prevState) { let { widthScale, heightScale } = prevState; if (nextProps.data.length !== widthScale.domain()[1]) { widthScale.domain(d3.range(0, nextProps.data.length)); heightScale.domain([0, d3.max(nextProps.data)]); prevState = { ...prevState, widthScale, heightScale }; } return prevState; }

It’s a static method, which means no this keyword for you. You're running on the class, not an instance. You get the new props and the current state.

Take widthScale and heightScale out of props, check that relevant data has changed, update their domains, put them back. Probably should update their ranges, too.

The tricky part here is that without ifs, you're recalculating scales on every update, which ruins any performance gains from putting D3 in state and using getDerivedStateFromProps.

Render your chart

Now that your scales are always up to date, you can render your chart. Same as usual, D3 for props, React for rendering.

render() { const { x, y, data, height } = this.props, { widthScale, heightScale } = this.state; return ( {data.map((d, i) => ( ))} ); }

Get chart coordinates, data, and height from props. Grab widthScale and heightScale. Return a <g> element full of rectangles.

Each rectangle is rendered in a loop and takes its coordinates and dimensions from our scales.

The result after a splash of color: A BarChart of random numbers where height and color correlate to the value.

I also made a button that lets you add random values to the chart. That way you can see that it’s updating perfectly declaratively. Update props and the chart updates. No need to understand implementation details.

You can play with it on CodeSandbox.

Pushing it too far

You can render 100,000 SVG nodes in CodeSandbox if you’re patient. Then you can’t edit your code anymore.

How it works for declarative transitions

Here’s a small example of building a transition with React 16.3. Using only approved lifecycle callbacks and the new ref API.

You can play with it on CodeSandbox 👇

The core issue we’re working around is that when you pass new props into a component, React re-renders. This happens instantly. Because the re-render is instant, you don’t have time to show a nice transition going into the new state.

You can solve this by rendering your component from state instead of props and keeping that state in sync.

Something like this 👇

We define a Ball class-based component. Its state has a single attribute, x, which is set to the props.x value by default. That way our Ball component starts off rendered at the position the caller wants.

Next, we create a new circleRef using the React 16.3 ref API. We use this reference to give D3 control of the DOM so it can run our transition.

That happens in componentDidUpdate.

componentDidUpdate() { d3 .select(this.circleRef.current) .transition() .duration(1000) .ease(d3.easeCubicInOut) .attr("cx", this.props.x) .on("end", () => this.setState({ x: this.props.x }) ); }

React calls componentDidUpdate whenever we change our component's props.

We use d3.select() to give D3 control of the DOM node, run a transition that lasts 1000 milliseconds, define an easing function, and change the cx attribute to the new value we got from props.

Right now, state holds the old position and props hold the new desired position.

When our transition ends, we update state to match the new reality. This ensures React doesn’t get upset with us.

At the very end we have our render() function. It returns an SVG circle. Don't forget to set the ref to this.circleRef.

Declarative 💪

We made sure our implementation is completely declarative. To the outside world at least.

Our state holds a flag that says whether our ball is on the left. If it is, we pass an x prop value of 15, otherwise 300.

When that value changes, the <Ball /> transitions itself to its new position. No need for us to worry.

If we flip positions during a transition, D3 is smart enough to stop the previous transition and start a new one. UI looks perfect.

Try it. 🏀

Cheers,

~Swizec

PS: Another cool article I wrote last week is about Using YouTube as a data source in Gatsby.


P.S. If you like this, make sure to subscribe, follow me on Twitter, buy me lunch, and share this with your friends 😀

Like what you read? Give Swizec Teller a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.