React: Managing Complex State Transitions With useReducer
I’ve recently started working on refactoring a D3 project that I built earlier this year called Streetball Mecca. The project itself is a rebuild of a Tableau dashboard created by David Siegel but with a few small design changes. I’ve always struggled with dashboard design so I often seek out existing projects to rebuild.
My version of the project was done solely in D3 and, although I was quite proud of the original rebuild, I’ve come to the conclusion that React would have been a much better choice to organize the project, as well as, manage much of the business logic using the useReducer hook.
Over the last year I’ve worked extensively with hooks and expanding my knowledge beyond useState to include: useReducer, useContext, useRef, useMemo. I’ve also taught a few intro to Redux classes so I understand the true power and benefit of incorporating some logic for state management.
The Existing D3 Codebase
My goal now is to take all that knowledge and refactor the D3 project within the framework of React and limit D3 to only the map and bar chart. Even then I’m going to render as many of those elements via React as possible.
The original D3 code was the following collection of files. All the files had one or more functions, some of which needed to be loaded first so they would be available in the scope for functions loaded later on.
Although the naming convention of the files helped direct my attention, when it came time to understand what I had done almost a year ago there were times I found myself searching for logic not quite knowing where I had placed it.
I won’t go any further into the code but will reiterate that almost every UI element and interaction was created via D3 in these files. Here is the link to the project in codepen.
The Initial Refactor
I did little planning and just jumped right in and started to code. I thought to myself that I’d build out the components one at a time and implement the logic as needed using useState to manage state.
Well, as you can imagine, that was not the best decision and I found myself with dispersed logic in several hander functions.
Here is what the original logic looked like in App. At a quick glance, someone who understands React will surely be able to make sense of the logic and this approach would continue to work regardless of how much more functionality was added.
However as I started to incorporate more functionality, this approach started to include more and more helper functions which needed to be passed to the hierarchy and started to appear bloated.
I knew there was a better way to manage state and the business logic and kicked myself in the ass for not having invested more time choosing useReducer to structure the code.
The Second Refactor
So now it was time for the second refactor. Once this portion was completed I was able to truly appreciate the power of useReducer and its ability to structure and organize the code into something that seemed more cohesive and intuitive to understand.
Using actions that are then dispatched to the reducer provided a central place to manage the state and business logic of the application.
Here is the parkReducer and some of the supporting helper functions. One quick glance over the code and you can clearly see the actions which, by convention, are written in uppercase. These actions define the business logic and are then dispatched by the child components.
The App codebase was then significantly reduced and needed to only import the reducer, instantiate a new instance, and pass it an initial state.
The reducer takes in the initial state and returns it as parkData. It also returns a dispatch function, which is used to pass actions to the reducer.
Actions tend to be objects and are assigned a key called type, also by convention, which contains the action to be performed. It also may carry a payload if data needs to also be passed to the reducer.
In this case the payload is the data retrieved from the initial API call.
The action once received is evaluated using a switch statement, which is another convention used for writing the conditional logic in a reducer. It then returns a new version of state.
One action that didn’t need a payload was the reset button.
Although I’m not yet finished with the refactor, I believe making the switch to useReducer was the right choice. I’ve also contemplated incorporating useContext, however, I’m just not finding a solid use case as the React hierarchy is only a few components deep and prop drilling seems to work just fine. I’m also only passing down a single dispatch function which only forces me to define one additional prop to pass from App.
Here is the CodeSandbox of the latest iteration of the project in case you want to give it a closer look.