Long-term React & Redux SPA — Lessons learned
Long-term React & Redux SPA — Lessons learned
In the last couple of years I was involved in several React & Redux projects. During this challenging period of time I faced and recognized common patterns, pitfalls and repeating use-cases.
All the efforts and time invested in development, refactoring, code reviewing and debugging totally paid off in the current SPA (Single Page Application), which I’m still being part of.
One year ago our front-end team started designing and developing the SPA from scratch.
Now, when we have a stable API, we decided to share our experience as lessons we learned.
Before we continue, it’s important to list the main tech requirements we had in the project’s beginning, in order to better understand the decisions we took:
- The back-end API is RESTful.
- The back-end data is highly relational represented and we knew that there would be a lot of Resources to manage on the front-end.
- The front-end should be implemented as a SPA.
Knowing the main requirements, now we can deep dive into the lessons.
Lesson #1: Reducing the Redux boilerplate
If you’ve played around for a while with Redux, I’m sure you’ve somehow faced the burden of managing a lot of actions, action creators and reducers similar by logic, but only differing by their naming and context.
Let me explain better. Having a RESTful API (or whatever API you’re using) it’s supposed that all the entities have the same interface for managing the CRUD operations (Create / Read / Update/ Delete).
So let’s say we have two entities for the example — Books and Authors.In order to support and manage CRUD with Redux, we have to write the following verbose boilerplate code for each single Entity.
For the sake of shortness let’s implement only the Book CREATE and DELETE actions, action creators and reducers:
As you can guess— the logic for the Author entity would be the same, just the naming will be different. Here’s how the actions and action creators would look like:
* Will keep the example short and will skip the repetitive reducers.
Having such an experience in the previous projects, we decided to develop utilities for creating actions, actions creators and reducers in order to reduce the Redux boilerplate.
So using these utilities, here’s how the code looks like for creating actions and actions creators for Books and Authors:
The same idea we applied to the reducers too:
Wondering where are the implementation details? Great!
While planning the article I had decided to include them, but later I figured out that there are already great community libraries for reducing the boilerplate, unlike the time when we started the project.
Our implementation looks very much like redux-arc project, so it’s a great starting point.
If you wanna build your own utilities, then I recommend you to check the official Redux recipe for Reducing Boilerplate.
Lesson #2: Containers on steroids
Generally speaking — the Presentational components are mainly responsible for how things look like, while the Containers are mainly responsible for Data fetching, computing and providing it to the Presentational components.
All’s good! But in the process of development we figured out very repetitive and duplicated flow in most of the Entities’ Containers. The flow is as follow:
- Container components call the API for the needed Entities.
- While fetching the Entities, we’re showing a Loader.
- When the Entities are Fetched we’re computing derived data with Selectors (reselect)
- Finally we pass the data to the Presentational components.
Here’s the flow represented with the code:
So we decided to move the Containers a step forward and abstract their repetitive flow. We created a Fetcher HOC, which is responsible for Entities fetching and showing the Loader.
Now having the Fetcher HOC, AuthorsContainer looks like:
As you can see — with this abstraction, the Containers are tiny and simplified and what’s most important — the flow is reused.
Lesson #3: Handling controlled and uncontrolled inputs
Managing many input fields in a long-term project, I would change the React docs statement to:
It c̶a̶n̶ ̶s̶o̶m̶e̶t̶i̶m̶e̶s̶ will be tedious to use controlled components …
Handling a simple controlled input field for example, it comes with repetitive and verbose boilerplate:
On the other hand, using the uncontrolled approach, would reduce the boilerplate a bit, but it’s not as flexible as the controlled one.
Whatever approach we choose, we have to create a stable component API in order to reuse the form field. We have to consider how to handle form validations, error handling, state sharing and other form related features. And these requirements are all valid for all types of form elements, such as — select, textarea and the rest input types.
As you can guess — we can create our own form components and mechanism, but for such a trivial task, should we invest time, instead of focusing on the business logic?
Of course we should not reinvent the wheel! There’re already more than great and feature-rich Form libraries doing it for us — Formik, Redux-Form.
We’re using Redux-Form in our project and we’re pretty happy we don’t handle it on our own.
Lesson #4: Managing relational data
In the project’s beginning we agreed in the team about most of the important architectural decisions, except one.
We thought it’s a trivial case and there’re already good practices around. Unfortunately , we didn’t find out a good approach and we decided to summarize our concerns and to ask the community (Stack Overflow):
* For better understanding of the case and code examples, please take a look at the question and its answers.
In short: Having 70+ models on the back-end (one-to-many, many-to-many), what are the good practices to represent the highly relational data on the front-end via React & Redux?
Let’s review the following example, in order to illustrate the case better:
- We have Books and Authors models.
- One Book has one Author.
- One Author has many Books.
As simple as possible. Here’s how the Redux’ Store would look like:
It would be a pretty straightforward task if we want to:
- Get all the Authors or Books from the Store, using a selector library (reselect).
But now imagine the case below, where the selector library isn’t flexible enough and we need something powerful and better as approach:
- Getting all Authors with their Books, which have at least one Book in a specific Category.
Also imagine if we add filtration, sorting and nesting other related models to the requirements. In addition, how can we simply reuse the custom joins we made in the selector body?
As you can guess the selectors can get messy and very complicated very soon.
As a result from the question and the discussions that followed, two possible approaches were derived:
- Creating indexes + selector library
- Using an ORM library + selector library
Creating and managing indexes of 70+ models is doable, but it’s not that simple! The idea here is to develop an utility, which will create indexes between the models by a specific criteria.
For example: having Books indexed by Category.It sounds like a simple task, but how can we create and manage the indexes between many pivot models, together with supporting filtration and sorting?
Such Indexing strategy sounds like creating a third-party library and it’s totally out-of-scope for the project.
Because of this we chose Redux-ORM:
A small, simple and immutable ORM to manage relational data in your Redux store.
The ORM makes the process of getting related models easier, but nothing is as perfect as it sounds.
Redux-ORM deals with relational operations as querying, filtering, etc. in a very easy to use way. Cool!
But when we talk about selectors reusability, composition, extending and so on — it’s kind of tricky and awkward task. It’s not a Redux-ORM problem, it’s more to do with the reselect
library itself and the way it works. Here we discussed the topic.
We continue using the ORM and still trying to push the selectors to their limits.
You can check my detailed SO answer here.
Lesson #5: Error reporting
Whatever we do, no matter what efforts we invest to ship stable production code, the bugs are always part of the project life cycle.
Having tests, code review sessions and pull requests not always guarantees us a bug free production code.
Therefore, it’s important to have an integrated error reporting service, that will help us fix the unexpected bugs on time.
So we integrated Sentry error tracking tool via Raven Middleware for Redux client library.
The library logs all the errors which occurred in the dispatching process along with the current Redux’s state. It’s flexible and easy to configure. For example, you can filter out the sensitive data before sending the error details to Sentry.
When an error occurs, in the Sentry’s dashboard we have details about:
- Error stack trace: