Yet another guide to reduce boilerplate in your Redux (NGRX) app
What are we gonna cover here?
Several ways/tips/tricks/ancient black magic rituals to reduce boilerplate in our overwhelmed-with-boilerplate Redux (and NGRX!) apps I came up with over the years of first-hand production experience.
Let me be honest with you, guys. I wanted to tell just about my new micro-library flux-action-class at first, but it seems like everybody’s been complaining how tech blogs more and more look like Twitter, how everyone wants some meaningful long reading and etc. So I thought: “What the heck? I got some experience and best practices of my own which I spilled some sweat and blood over. Maybe, it could help some people out there. Maybe, people out there could help me to improve some of it.”
Let’s take a look at a typical example of how to make AJAX requests in Redux. In this particular case let’s imagine we wanna get a list of cats from the server.
If you’re wondering why I have selector factories (makeSelector…) take a look here
I’m leaving out side effect handling on purpose. It’s a topic for a whole different article full of teenager’s anger and criticism for the existing ecosystem :D
This code has several weak spots:
- Action creators are unique objects themselves but we still need action types for serialization purposes. Could we do better?
- As we add entities we keep duplicating the same logic for flipping
loadingflag. Actual server data and the way we want to handle it may change, but logic for
loadingis always the same. Could we get rid of it?
- Switch statement is O(n), kind of, (which is not a solid argument by itself because Redux is not very performant anyway), requires couple extra lines of code for each
caseand switches can not be easily combined. Could we figure out something more performant and readable?
- Do we really need to keep an error for each entity separately?
- Using selectors is a good idea. This way we have an abstraction over our store and can change its shape without breaking the whole app by just adjusting our selectors. Yet we have to create a factory for each selector due to how memoizaion works. Is there any other way?
Tip 1: Get rid of action types
Well, not really. But we can make JS generate them for us!
Let’s take a minute here to think why we even need action types? Obviously, to help the reducer somehow differentiate incoming actions and change our state accordingly. But does it really have to be a string? If only we had a way to create objects (actions) of certain types… Classes to the rescue! We most definitely could use classes as action creators and do
switch by type. Like this:
All good, but here’s a thing… We can no longer serialize and deserialize our actions. They are no longer simple objects with prototype of Object. All of them have unique prototypes which actually makes switching over
action.constructor work. Dang, I liked the idea of serializing my actions to a string and attaching it to bug reports. So could we do even better?
Actually, yes! Luckily each class has a name, which is a string, and we could utilize them. So for the purposes of serialization each action needs to be a simple object with field
type (please, take a look here to learn what else any self-respecting action should have). We could add getter
type to each one of our classes which would use class' name.
It would work, but this way we can not prefix our action types as this great proposal suggests (actually, I like its successor even more). To work around prefixing we should stop using class’ name directly and create another getter for it. This time a static one.
Let’s polish it a little to avoid code duplication and add one more assumption to reduce boilerplate even further: if action is an error action
payload must be an instance of
At this point it works perfectly with NGRX, but Redux is complaining about dispatching non-plain objects (it validates the prototype chain). Fortunatelly, JS allows us to return an arbitrary value from the constructor and we do not really need our actions to have a prototype.
Not to make you guys copy-paste
Tip 2: Combine your reducers
The idea is simple: use combineReducers not only for top level reducers, but for combining reducers for
loading and other stuff. Let the code speak for itself:
Tip 3: Switch away from switch
Use objects and pick from them by key instead! Pick a property of an object by key is O(1) and it looks much cleaner if you ask me. Like this:
I suggest we refactor
reducerLoading a little bit. With introduction of reducer maps it makes sense to return a reducer map from
reducerLoading so we could easily extend it if needed (unlike switches).
Tip 4: Have a global error handler
It’s absolutely not necessary to keep an error for each entity individually, because in most cases we just need to display an error dialog or something. The same error dialog for all of them!
Create a global error handler. In the most simple case it could look like this:
Then in your side-effect’s
catch block dispatch
ErrorInit. It could look like this with redux-thunk:
Then you could stop providing a reducer for
error part of cats' state and
CatsGetError just to flip
Tip 5: Stop memoizing everything
Let’s take a look at a mess we have with selectors one more time.
makeSelectorCatsError because of what we discovered at the previous chapter.
Why would we create memoized selectors for everything? What’s there to memoize? Picking an object’s field by key (which is exactly what’s happening here) is O(1). Just write a regular non-memoized function. Use memoization only when you want to change the shape of the data in your store in a way that requires non-constant time before returning it to your component.
Memoization could make sense only if computed some derived data. For this example let’s imagine that each cat is an object with field
name and we need a string containing names of all cats.
Let’s take a look at what we started with:
And what the result is:
Hopefully, you found something useful for your project. Feel free to communicate your feedback back to me! I most certainly appreciate any criticism and questions.