I’ve had the opportunity to rehabilitate a number of React-based web applications built with Redux in recent years. Like many codebases, these apps have their share of problems, but I’ve observed there are a few particularly common themes. Fortunately, there’s a straightforward fix for these problems, and on a few occasions, this fix has left me with happy clients.
First, let’s look at what often goes wrong:
Problem Theme 1: Data Management
* How do you initiate requests to your API? Initiating a fetch request when a component mounts is tempting but often causes problems.
* What can you do to manage the concurrency of these API requests? It’s probably not ideal to have multiple in-flight requests for the same data, for example.
* How do you manage loading states?
* Once you’ve got data loaded, how do you pass it around into different UI components? If you choose prop-drilling, your code can become tedious and hard to follow. If you choose to store it in Redux, you now have a lot of extra work. How do you structure your app state? To what extent do you normalize data? Now you’ve got to define a bunch of Redux actions and reducers to wrangle all this, too.
While this infrastructure is critical, it’s also hard to get right. From what I’ve seen, it’s all too common to have ad-hoc half-solutions to these problems; the end result is a large amount of messy code that is hard to effect change in.
Problem Theme 2: Poor API Performance
A natural consequence of the pain of API interface and data management is the temptation to make the API perform more heavy lifting in support of the UI.
Some examples of this include:
* Fewer endpoints that load a larger set of data.
* Endpoints that pre-process data into a structure that mirrors the structure of the UI components. Or at least the _initial_ structure of the UI.
* Pushing some UI display concerns into the endpoints.
The net result of this is that your endpoints use more memory and CPU and perform a higher number of database queries. It’s common to see some n+1 queries sneak their way in these endpoints, too.
During development, testing, and initial production use, everything runs fine. Over time, as the system gains users and stores more data, the wheels start to fall off. The entire app feels slow, the server occasionally times out, and the users’ perception of the quality of the system is low.
Problem Theme 3: Mismatched Data
The last theme that often begins to crop up is the difficulty of keeping the API and UI in sync, in terms of the structure of data. Given the previous themes have increased the overall complexity of the structure of your API’s data, these themes all compound together.
Subtle bugs and reduced development velocity become the reality.
Unfortunately, the only real fix is to fundamentally address the missing pieces of infrastructure. This is going to take some work.
There is good news, however: there are well-supported and popular tools that fill in these gaps. Rather than design your own solutions, you can set up these tools and refactor code to make use of them. Here’s my currently favored approach:
* Introduce GraphQL and use Apollo Client in your UI. This immediately solves a majority of problems related to API access, data management, and extraneous use of Redux.
* Use graphql-code-generator and typescript in your UI. Preferably use their typed-document-node plugin. This will help you eliminate data structure mismatches.
* Use dataloader or equivalent in your server’s GraphQL resolvers to fix n+1 queries
* Introduce a data caching layer in your server. Often, this fits right into your dataloader layer.
And that’s essentially it. Admittedly, some of this is easier said than done. In some circumstances, implementing a proper dataloader can be challenging. Beyond that, you’ll have to refactor or rewrite a nontrivial chunk of code.
When the end result feels like a fast, brand new app without requiring a full rewrite, this price is well worth it.