Hooked on Hooks
By: Will Carter
Tags: React, JavaScript, Hooks, Mapbox, D3js

React.js Function components + Hooks + D3.js Data Visualizations + MapBox GL JS Web Maps

Challenge

A challenge I have recently explored in React.js based web development is the integration with powerful imperative libraries such as D3.js 😍 for building data visualizations and MapBox GL JS 💛 for dynamic web maps. I am a huge fan️ of both of these libraries and have been looking for the best patterns to implement them together in a modern React web app. In short, problems arise because D3.js and MapBox GL JS work by mutating HTML’s Document Object Model (DOM), which React wants ownership of. Additionally, there are issues with the imperative nature of these libraries, which are in conflict with React’s declarative style.

On the D3.js official website, it states…

D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation.

Similarly, Points of Interest, the official MapBox blog elaborates…

What can be confusing is connecting React with other libraries that manipulate the DOM and manage state (like Mapbox GL JS). How do you communicate between both effectively? It’s common to turn to component wrappers that provide a level of abstraction that hides this confusion away. -September 2017

As the MapBox quote mentions, solutions to these issues have been centered on the use of third party wrapper libraries that abstract away these issues (see React-Vis, react-mapbox-gl, react-map-gl, react-d3-components, among many others). For a deeper discussion, I recommend checking out Getting the heck out of React from November 2018, which states:

I see numerous attempts to “solve” the challenges of working with stateful media in React by creating libraries convert imperative APIs into a set of declarative React components… They do this by wrapping another layer of abstraction around 3rd-party tools and native APIs which provide limited on-rails solutions that serves to comfort developers with a React mindset.

While wrapper libraries like these can be effective, choosing the correct library can be a challenge. Also, in some cases, the libraries might only expose a subset of the full features of the native library. Key features, like smooth D3 animations, might not be easily attained.

Fortunately, recent updates to React in the form of Hooks provides paths for more simple solutions to these issues. Here are the goals of this project:

Goals

Provide a simple method of interaction between a D3.js based React component and a MapBox GL JS based React web map component. Use the React’s newer function components as opposed to class components. Understanding and use of the useState, useRef, and useEffect hooks will be attained. Use only native D3.js and MapBox GL JS libraries. No third party D3.js or MapBox GL JS wrapper libraries will be used. We want to full access and use of native D3.js capabilities. This includes all of D3’s strengths including: fast DOM manipulation and interaction, element transitions and smooth animation. Lastly, I want to get some experience with the different online React based web editors and see how well they work.

Interaction

The following screen grab shows the interaction we are looking for. We want to be able to mouse over or click on a bar in the barplot and have the matching feature highlighted in the map.

Click a bar, see it highlighted on the map

TLDR

Just want to take a look at the code? No problem. Check out the live demo, CodeSandbox, and GitHub for this project.

Data

The sample data for the project is based on population density of counties in Oregon. The source for the county geographic data can be found here and the population numbers can be found here.

After importing the data into our POSTGIS geo-enabled PostgreSQL database, we can join the data, calculate the population per square mile column and import the query as a map in QGIS.

Querying geo-enriched data, loading result in QGIS

Oregon counties

From QGIS, we can export the query layer as geoJSON…

Exporting from QGIS to json

…which we can use directly in our React application as our data source.

Data in place in our app

Code

First, let’s take a look at App.js. The json data from the previous step is referenced here. We formatted the source data to send to one version to BarPlot component and a full geoJSON version for MapboxGLMap inside of our data component.

Sending data to the components

Notice the use of the useState hook in App.js. We use this hook to keep track of the selectedId of the bar in the chart that the user interacts with in the barplot. We do this by passing the setter function (setSelectedId) to BarPlot, so that selectedId can be assigned inside that component when the user interacts with a bar in the bar plot. Additionally, we send the selectedId to the MapboxGLMap component so that we can highlight the corresponding county feature in the map.

Keeping track of the id of the bar interaction

Inside of the BarPlot component, the SVG based visualization is built using D3 functionality. Notice that we loop through the data property sent to the component to render individual Bar components. Also, the id and onSelectItem function is passed down to each Bar, so it can be called when the user interacts with it.

Passing onSelectItem to each Bar component

Further, inside each Bar component, we use the useRef hook to assign the rect element to barRef. This allows barRef to persist for the full lifetime of the component and will allow DOM mutations to that element to occur (via D3.js) without conflicts with React’s virtual DOM.

Connect barRef to rect element

Next, inside of the useEffect hook block, we use the native d3.select() to assign the current property of barRef to the element el, which allows us to perform side effects in function components inside of the useEffect() block. Here we use D3 to conditionally apply a transition to the element and to set its style when highlighted (setHighlight function).

d3.select()

If the itemDelay passed into the component is > 0, we apply the native D3 delayed transition to the element, with easing for the bouncy animation.

d3.easeElastic() animation

This transition delay and the easing create the bouncy effect on the bars as they appear in the visualization.

Finally, when a bar is interacted with (mouseover, click, mouseout), we call the onSelectItem(id) function that is passed to the component, sending the id of the bar to the function back up to App.js, stored in state. This id matches the feature id in the map component.

Sending selectedId up to App.js

Here you can see the selectedId value being output to the console when the bar is being mouseovered. This console.log(selectedID) occurs in App.js.

selectedId is updated in App.js

As mentioned earlier, this selectedId is also passed to the MapboxGLMap component in App.js, which drives the selection of the matching feature in the map.

Here we see the MapboxGLMap component. The property, selectedId is being passed to this component. Similar to the Bar component, we use the useRef hook to cordon off an element of the DOM that will persist over the lifetime of the component that can be safely mutated, this time by MapBox GL JS.

For this component, we want to use the useState hook to store an initialized map object in state to avoid re-initializing the map on each render. This happens in the useEffect() block within the initMap() function, which we only want to run only once after the first render of the component. This map initialization can be thought of as as effect, and is run in the useEffect() block.

The initMap() function does the following:

Initializes a new mapboxgl.Map and ties it to mapContainer.current, created with the useRef() hook. Loads the data to the mapboxgl.Map object. Adds 3 layers to the map object based on the data passed in. The third layer added is a transparent line layer, which we will update later to highlight a feature when the selectedId prop changes. Assigns the mapboxGL.Map object to the state variable, statefulMap.

initMap()

Next, notice the optional array [statefulMap, selectedId] passed to useEffect() block at the end. By default, the useEffect() block will run after every render of the component, but by passing this array, we can tell the effect to run only if statefulMap or selectedId changes.

By passing this array, useEffect() will only run if statefulMap or selectedId updates

This is similar to checking for changes in state variables in the componentDidUpdate() function of class components, as is discussed in the official React Hooks documentation.

In detail, this is what is happening in the useEffect() block:

On initial render of the component, if statefulMap has not been set yet, initMap() is run, which in-turn sets statefulMap in state. Because statefulMap is updated at the end of initMap(), the useEffect block runs again. But, this time statefulMap is set, so initMap() doesn’t run and it falls into the condition dealing with selectedId. We want initMap() to run only on the initial render. Initially, because the user is not hovering over a bar in the visualization, the selectedId sent to the component is null and the effect doesn’t continue to highlight a feature in the map. However, if the user does mouseover or click a bar, the selectedId passed to the component changes, causing the useEffect() block to run again. This time, selectedId is not null, and the effect block continues to select the specific feature.

initMap() then check for selectedId

Here you can see the process in action, as I have added console log statements identifying when things happen. I have made the console output larger and the play speed is cut in half in this screen grab so it is easier to see the sequence of events.

Tools

I was impressed with CodeSandbox.io, especially with its ability to connect directly to a GitHub repo. The code and a demo of this post can be found here.

For a great comparison of the features of online code editors, check out this recent post by Curran Kelleher.

Finding the “perfect blend” of features

Don’t forget to check out VisHub 2.0, which is feature rich yet simple enough to nicely showcase visualization demos, especially in the areas of D3/React. Incidentally, Curran has a wealth of related tutorial content and I enjoy learning from him. Check out his recent video, Interaction with React. 🙌

Conclusion

How did we do with the goals?

Provide a method of Interaction between a D3.js based React component and a MapBox GL JS based React web map component.

Attained. This post has been a simple presentation of an interaction path from a D3 based component to a MapBox GL JS, based on using a few React Hooks. Perhaps Redux.js is in order here, where the vis component would notify the change of selectedId to a more global state rather than the Bar component notifying it’s parent of the change and having it go up the chain to App.js. Redux is a current area my exploration into React web development.

For a deeper dive, check out Interaction with React, by Curran Kelleher.

Use the React’s newer function components as opposed to class components. Understanding and use of the useState, useRef, and useEffect hooks will be attained.

Attained. I have been a fan of function components since entering the React ecosystem. Hooks looks to offer function components the powers that only class components had before. Namely: maintaining state (useState), useRef (surrender DOM element to be under control of another process, not React), useEffect (combines componentDidMount, componentDidUpdate, componentWillUnmount from class components, allows running side effect (data loading, map setup, stuff you want to do only once after component renders).

I have tried to explain the details here so as to gain full understanding myself.

Use only native D3.js and MapBox GL JS libraries. No third party D3.js or MapBox GL JS wrapper libraries will be used.

Attained. No wrapper libraries used.

We want to full access and use of native D3.js capabilities. This includes all of D3’s strengths including: super fast DOM manipulation and interaction, element transitions and smooth animation.

Attained. The transition/delay combo in this project shows an animation with bounciness thanks to d3.ease.

In researching the component libraries for this post, I came across a promising open source library/project that leverages key D3 libraries for it’s animation capabilities: Victory: React.js components for modular charting and data visualization. In searching their GitHub repo for “d3”, I find in references to d3.ease and d3.interpolate. I wonder if the addition of Hooks to React changes anything for the Victory library. I see that many of the components in the library are classes, not function based. Perhaps they could be converted to function based now?

React based web editors?

Attained. I could have looked at more, but am pretty happy with CodeSandbox.io and excited to see what’s ahead with VizHub.