Caveats of ngrx
Recently I was helping to estimate an Angular project for a prospective client. In the specification they provided to us there was a sentence that resonated with me:
“In order to avoid problems with mutation and asynchronicity we decided to use in our project Redux…”
The team from the client’s side are strong experienced developers, but they are just paving their way into Web development, moving their first consumer-facing application to Angular (in the quote above I preserved the original sentence that mentions redux, although they really meant ngrx). They are in the process of learning Angular, TypeScript, countless number of tools, and even brushing up basics like HTML and CSS.
I genuinely think ngrx (or more precisely the pattern pioneered by redux) addresses some issues in a unique way. If the client insist we’ll use ngrx, but I’d prefer to avoid using it in our projects. I discuss the reasons below, but if you ask me for a single major reason — I would argue that vanilla Angular is already good enough at dealing with the problems that ngrx aims to solve.
Below are the considerations I felt obliged to share with the client.
First of all, to respond the statement about async problems — ngrx doesn’t add or remove anything when it comes to async problems. Actually what problems?! Maybe I’m spoiled by RxJS, but all the async “challenges” we have to deal with in Angular are perceived as routine tasks. In 2 years of working with Angular I’ve spent zero time debugging async issues.
State mutation problems
They do exist. However that’s where I think vanilla Angular is already good enough. 100% of my Angular components use OnPush change detection strategy by default. It prevents you from chaotically patching state object’s properties. You just stop seeing any changes on the page. Instead when I need to modify data I create a new instance of an object that my component receives as an input property.
There are cases when it is more convenient to use Default change detection strategy then I just switch back to it! When it makes sense I even don’t hesitate to manually trigger change detection 😱🙀 with ChangeDetectorRef! Very rarely though.
Is it the same as immutable ngrx store? No. Does it provide the same level of control over your data as ngrx? No. Does it effectively help you to avoid state mutation problems? Yes, I believe in most cases it’s good enough.
ngrx breaks encapsulation
All your actions, stores, reducers live in the same central place (will be glad to be proven wrong, but I haven’t seen an alternative approach so far). Sure you can split them into files, files into classes, classes into methods, but at the end of the day they all reside in the same top-level application module. Now imagine you have an admin module with rich functionality that you load on demand. Even if 90% of your visitors never use it, it still will be loaded along with the main bundle on the application startup.
It also contradicts with one of my favorite quotes:
If everything the admin module needs would be written inside that module I could just delete the folder if I don’t need it anymore. With ngrx I’ll have to cleanup leftovers from the main module as well.
Encapsulation is not about deleting code though it’s more about managing complexity and maintainability. It enables you iteratively improve design and implementation without drowning in the technical debt. New people can easier join the project when all the modifications they need to do are scoped to a limited area. Can’t say that you lost all these with ngrx, but I think it becomes substantially harder.
Tends to be messy
Actions and reducers can quickly become a mess without thorough care. You may say the same is true for regular components and services, but I don’t think so for two reasons.
First, components and services look pretty much the same in every Angular app. In contrast every ngrx project I’ve seen so far has its own opinion on how to implement and organize actions, reducers, store selectors (or not to use them at all). Actions can be defined as variables, classes, interfaces, enums. Directly exposed as ES module members or grouped into classes. Helper functions to avoid duplication of unique action type strings. Similar zoo for reducers.
Second, actions and reducers is additional code that wouldn’t exist in your app otherwise. It’s not the code that exists in your app anyway and you just move it from components to external entities. So if your components are a mess then you will double it.
ngrx considerably steepens the learning curve. It’s not only the @ngrx/store that you need to use, to fully leverage benefits of this approach you need to adopt @ngrx/effects and @ngrx/router-store. Not a rocket science, but if you are already in the process of learning Angular, TypeScript, all the tooling, it would be wiser to postpone adding libraries that you can live without.
Not backed by Google
That’s not a concern for me, but something a big enterprise company should consider before making a decision. ngrx is a serious dependency, it influences your application architecture, creates foundation for the entire app. It’s not a left-pad type of library that you can replace within a day. What if it won’t be actively supported in a year from now and will become incompatible with future Angular versions that you will require?!
Other minor things to consider
- The community is much smaller than for “mainstream” Angular development. It may take more time to resolve issues and questions that arise.
- From day one you need much deeper knowledge and understanding of RxJS. Regular Angular approach soften this requirement.
- Can lead to mixing mutable / immutable approaches. Wiring up ngrx actions and reducers with UI requires more time, developers may do what’s quicker in the rush before a deadline.
- Since the state is global for the entire app, you need to come up with a way to clean it up when user moves from one part of the app to another to avoid waste of memory.
- Often times you’ll need to provide
ngForTrackByfunction to avoid performance degradation and unwanted enter animation effects. Otherwise DOM generated by
*ngForwill be destroyed and re-created every time the bound collection is modified.
- It may decrease productivity. With ngrx you are exposed to one of the hardest programming problems — naming 😉. To change the state of an input field you may need to come up with a name for action type (unique string that identifies the action), action payload interface, action group (to scope actions), action class, reducer 🙃. Hopefully your input field doesn’t have an invalid state that will require another set of action names.
Not all Angular projects benefit from ngrx (and generally from this approach). Consider all the downsides that may not lie on the surface. Unless you have a clear evidence, do not try to prematurelly solve the problem that you do not have. Angular already comes with means for dealing with various architectural challenges.