NgRx is an amazing implementation of the redux pattern using the RxJs library. Redux takes planning to implement, but the payoffs in reduced complexity is where the ROI is at. Far too often I hear about developers complaining about “boilerplate” and how much work it is to implement. Often, these developers will have the same service calls repeated across many components where they subscribe to the observable, set variables upon it resolving, and write custom error handling. This code is repeated over and over, and is accepted as ok.
Here is my truth about NgRx. I’m going to have to plan out what my entities will need to do. This often includes loads, saves, etc. I will then create actions, reducers, effects, and selectors to support this functionality. I can use schematics like @briebug/ngrx-entity-schematic to generate this for me if I like. I’m going to experience a little pain up front and a LOT of simplistic code when I go to use it. If I’m not willing to pay this price for reduced complexity, then I pick something else. As an alternative to NgRx, I would recommend the service with a subject pattern, as it gives me an upgrade path to NgRx if a functional area of my app needs it.
You take shortcuts
I still see people do really crazy stuff to avoid the NgRx pattern while still implementing the NgRx pattern (sort of). One common scenario is that developers will decide that they only need one action and they handle everything in the store through some “update” action. I was recently conducting a code/architecture review with a Fortune 500 company and was looking at their store code. I opened up one of the action files and it had a single action in it: “UpdateThingamajig”.
In disbelief, I then proceeded to look at the reducer for this entity, and to my surprise, it only had a single action implemented that spread the existing state and replaced it with the payload for “UpdateThingamajig”. The effects file -- you guessed it -- had a single effect method that did some data manipulation before the reducer received the dispatched action. I had the opportunity to see the real “magic” when I opened the component files. Each component was responsible for managing how state would change before dispatching the ubiquitous “UpdateThingamajig” action.
They successfully circumvented needing to create actions, reducers, and effects for their thingamajig entity, but duplicated state management code throughout their components. This is an anti-pattern that results in far more effort to create and maintain than just creating proper state objects.
If you want shortcuts, pick another library and follow best practices for that library. More often than not, shortcuts create a maintenance nightmare that is difficult to debug and almost impossible to test.
You nest your data in the store
Data that comes from an API is always perfect, said no frontend developer ever. As developers we are often given a square peg for data and expected to make it fit into a round hole. Worse, we often don’t have control or any say about the APIs we are required to use, and we must make the best of what we are given. That said, we still have to make wise choices.
When we have nested data in our store, say a list of blog posts, where each blog item has an author property with imbedded user information for who created the blog, plus an array of comments, each with an author, etc, etc. Sometimes our data comes to us this way, but often we decide we can take another “shortcut” and attach/nest our related data to reduce how much code we have to write. When data is nested in NgRx, it may appear that it makes things easier, but in reality you’re making things harder.
First thing you will likely notice with nested data is that your reducers become much more complicated. You will no longer be able to spread your existing state and then update the properties you want with one or two lines of code. Instead, you will have reducers that are exponentially larger, difficult to maintain, and very tough to test. In addition, you will have duplicated data in your store and your reducer will have to find all the instances where a user record is attached to a parent record and then update it to keep it in sync. Another side effect of these complicated reducers is that it becomes much easier to mutate your state, and without tools like store-freeze, you may not catch the issue until much later.
Another problem with nested data is a result of good component design. When we think about our applications, we start with a top level app component, which renders a page component, which renders a bunch of child components, which are often nested multiple layers deep to reduce complexity and promote reusability. The component pattern often follows the container/presenter model and allows us to create reactive forms/pages. This is a great pattern to follow and I teach it often to developers of all levels. The problem with this is that since the data in the store is immutable, if I change a single property on a nested object, the whole state object has to change, and thus forcing a rerender of potentially hundreds of components. This is a big performance issues and can easily be avoided.
To avoid nesting your data, look to use the @ngrx/entity library, which converts your data from array based data to dictionary based data. Each item of the array is now stored in a dictionary where the unique identifier is the key and the value is the item. I can now easily find data in my dictionary using square bracket notation to locate the item I am looking for. It’s highly performant, prevents data duplication, simplifies reducers, and is easy to work with.
You don’t leverage selectors fully
If your store is like a client side database, then selectors are like queries. They allow you transforms, reduce, filter, or roll up your data in whatever way you like. They are composable and memoized to keep them lean and performant. This feature is what makes NgRx stand out from other architectural patterns: the ability to share, reuse, and transform data without having to go back to the API.
One faux pas I see with selectors is where developers will dispatch a new action to retrieve the same data so they can transform its structure or get a filtered subset, including a single record. This can often be done with selectors without having to go back to the API which saves time and cycles updating the store. In some cases, like loading a single item by id, it might be best to go back to the API to get a “fresh” instance of the data, but make sure that you need to do this vs. just making it the default.
The other way I see selectors not being used is in the effects. Often I will need additional data from the store, so I will use a selector in my component to get it so I can pass it with my action. The data is completely unrelated/unused by my component and only needed by my dispatched action. In this case, don’t have the component use a selector to retrieve it only to pass it along as that will required a subscription to extract the value and it’s not needed elsewhere. A better approach to consider would be to use `withLatestFrom(store.pipe(select(mySelector))`. This allows you to get the previous value from the store as necessary, simplify your actions, and remove unnecessary code from your components. This technique can also be used to check to see if a record already exists in your store before making a call to an API to retrieve it.
NgRx is a powerful library that implements the redux pattern. NgRx solves many age old problems, including race conditions, sharing data between components, and component-to-component communication. By following best practices with this pattern, developers can create powerful applications that are performant, with reduced complexity, and maximized component reuse. <happy-coding/>
You can reach me:
Follow me on Twitter: @JesseS_BrieBug
Companies like BrieBug focus solely on Angular to help our enterprise clients navigate training, architecture reviews, and implementation of their important web applications. BrieBug is one of the top web application development firms in Colorado. For more information, contact us.