React & D3: Experimenting With The useD3 Custom Hook

As of late, all my attention has been focused on working through a full refactor of Streetball Mecca from the original D3 only version to the newer React & D3 version. In the process, I’ve come across quite a few articles that discuss how to incorporate D3 into React, one of which included a custom useD3 hook and so I decided to perform one last refactor to the map and add the new hook to the mix.

In my previous article, React & D3: Rendering A Map, I discussed two approaches for incorporating D3 code into React. The initial approach, D3 within React, provides D3 with as much control as possible in rendering the data, but still requires the help of React’s useRef and useEffect hooks.

The second approach, D3 for the math and React for the DOM, gives React more control and calls upon D3’s specific helper functions. In the case of the map, it was d3.geoMercator for the projection and d3.geoPath for drawing the paths. I also decided to include d3.scaleOrdinal() to provide the mapping of boroughs and their assigned colors. In the end a simple object that mapped borough names as keys to corresponding colors would have sufficed, but I miss working with D3 scales, and wanted to keep that skillset sharp.

Both solutions rendered the following map, including the circles.

Neither is better or worse for this type of visualization and for someone that works with React it’s hard not to want to coerce the code into React, allowing it to manage state, render the DOM, and update accordingly. All the while forcing D3 to take a back seat. However, for those well versed in D3 yet new to React they must first overcome the new learning curve.

The useD3 Custom Hook

The article I came across was “Using D3.js Inside a React App” by Benny Au. In it, he creates a custom useD3 hook that renders a vertical bar chart with x/y axes. Since the hook seemed to follow the D3 within React approach, I decided to refactor that solution and see how it might improve the existing codebase.

I decided to only render the Map using the hook and keep the existing useEffect to render the circles. My goal was to see how to work with the new hook and then to decide if it was worth the time to do any additional refactoring .

Since coding is a process and as one continues to explore other possibilities they may come to see their code in a new light. I also saw this as an opportunity to document what I learned along the way.

The Existing D3 within React Code

Since the useD3 hook article fell in line with the D3 within React approach, I refactored that solution rendering both the Map and Circles. I’ve collapsed the code for both renderMap and renderParks for the time being.

The things to highlight here, relevant to incorporating the useD3 hook, are the svgRef and useEffect, both of which will be migrated into the useD3 hook. The renderMap function will basically remain as is with only a few small changes.

Setting Up The Hook

The first step in the refactor was to create and import the hook. I created a useD3.js file and copied/pasted the existing code right from the article. I also decided not to make any edits to the useD3 code as I wanted to test it’s its reusability. If it could render the map, then it should be able to render the circles and bar chart somewhere down the line.

Inside The Hook

As we can see a new ref is being created, which also becomes the return value of the function. useEffect is being used to call the function used to render the chart and does so based any changes to its dependency. One change that will require a small refactor in renderMap is that renderChartFn now passes the current svgRef reference as d3.select(ref.current).

Using The useD3 Hook

So in order to keep as much of my code intact, I assigned the existing svgRef the return value from the useD3 hook and passed it the renderMap function and [data.length] as the dependency.

I then needed to make a few edits to the renderMap function. This included the following:

  • converting the renderMap function from an expression to a declaration as it’s now being called before it’s initialized
  • replacing the existing params with svg as the useD3 hook passes d3.select(ref.current) to the callback function
  • replacing d3.select to reference the svg param, which will be a d3 reference to the to existing svg (d3.select(ref.current))
  • adding conditional logic (if (data[0)) to run only when the data exists
  • update d3 to reference the features array during the data binding data(data[0].features)

Here is what the code looked like after making those few edits:

Wrapping My Head Around It

The initial setup required a little troubleshooting as things didn’t fall right into place the first time around. As I was working through the errors I meticulously placed console logs, as I always do, in order to validate my assumptions but also realized they helped visualize the flow of execution. I’ll talk about this shortly.

Although I removed the console logs from the previous code snippets, I’ve added them back in to walk through what gets executed and when. I’ve recently gotten into the habit of adding descriptive text in my logs which takes the pattern of:

Component or Function Name — where it’s located — the variable(s) it’s outputting.

I can’t stress enough how this has helped me work through errors and even provided a reference to where I need to go back and comment out the console logs once the issue has been resolved.

Flow Of Execution

Anyone new to React may not yet understand the flow of execution in a component including initializing and updating state, rendering the DOM, and then of course the useEffect lifecycle methods. Hopefully this walkthrough might help explain things.

The first console log shows the current state of the data that is initialized by the useDataApi hook, which happens before the useD3 hook is instantiated. We can see that it contains no data as of yet and is an empty array.

Map — before useD3- data []

This is followed by a console log placed inside the hook just before useEffect. The output is that of the current state of the ref, which is undefined, and the dependency value which is [0].

useD3(before useEffect) — ref, dependencies {current: undefined} [0]

The reason the ref is undefined is because React has yet to return any JSX, which is when it binds the svgRef to the actual svg element. The dependency is [0] because it is passed data.length, which is empty.

The next output is just before Map’s return statement. I added this here to confirm the flow and to validate that useD3’s useEffect hasn’t run yet, and will not do so until after the Map component returns JSX.

Map — before return — data []

Now, as expected, once Map calls the return statement only then is the useEffect called.

useD3(after useEffect) — ref — dependencies {current: svg#boroughs-map}[0]

Now that useEffect is called it runs its codeblock calling renderChartFn, which is technically the renderMap function. Inside of that function I placed another console log. Here we can see that there is still no data as of yet which is why I needed to include the conditional logic if(data[0]).

The useEffect was configured to run on the initial load (componentDidMount) and during any updates to its dependencies (componentDidUpdate), but the code would break as there was no data to bind to using d3.data.

Map — inside useD3 CB — svg Selection {_groups: Array(1), _parents: Array(1)} []

Once state has been updated via the useDataApi hook, the component re-renders and the entire cycle repeats itself.

As we can see data now exists and the dependency has been updated from [0] => [1] causing useEffect to execute its codeblock.

Conclusion

Although the useD3 hook is useful I would need to work out additional logic on how I might use it to render the circles as well considering a separate useEffect is currently being used to do so which has a separate dependency.

Usefulness

I think the hook does a good job in migrating some code out of the component and thus cleaning it up a bit. It’s also reusable, which I haven’t yet tested but will do so shortly enough.

Performance

One thing that I did notice was that once the useD3 hook was in use the circles lost their animation which is an aesthetic I’d like to keep. When I had originally refactored the circles from D3 within React to D3(math)/React(DOM) I did need to leverage React-Spring to perform the animation. My guess is that this current issue is related the React rendering process.

Anyway, here is the CodeSandbox solution that includes all 3 refactors. All you need to do is go into App and comment in the one you want to test. As always I’d love to hear any feedback/suggestions regarding the code.

Software Engineering Instructor at General Assembly. React and D3 evangelist and passionate about sharing all this knowledge with others.