Level Up Your NgRx Skills With 10 Time-Tested Best Practices

Level Up Your NgRx Skills With 10 Time-Tested Best Practices

NgRx is the most popular state management library for Angular, let's explore set of real world proven best practices to level up your skill!
  • angular
  • ngrx
Nature knows the best! Be like nature 😉 (📷 & 🎨 by Tomas Trajan)

Hey folks! 👋

Some of you might have realized my article publishing frequency went a bit stale for quite some time but you can be sure it was for a good reason… Especially because there is a silver lining to all of this!

Last two years, I have spent all my free time developing OMNIBOARD — the best tool for software engineers that helps them to understand and evolve polyrepo environments of their enterprise organization, it has a pretty powerful free plan with unlimited projects so make sure to check that out! 😉

OMNIBOARD allows you to analyze all your project repositories with custom checks and create dynamic dashboards to understand your environment, track progress and evolve code bases

As you might have guessed, my preferred tools to work with are Angular & NgRx and that’s exactly what I used to build Omniboard!

Developing a SaaS product as a solo developer allowed me to gain valuable experience while being forced to be brutally honest about what does and doesn’t work!

And that’s exactly what I am going to share with you now!

While you might have figured out many of the best practices by yourself when using NgRx in one of your projects or by reading about them as there are many great NgRx articles out there, You can bet there is always that one or two things which are going to be new and worth learning, plus there will be realistic code examples for every tip!

⚠️ ☕☕☕☕☕ This is a pretty long article (~15min) so don’t worry about reading the whole thing in one go, just come back when you need to refresh specific part you’re working on.

TL;DR

  1. Use schematics to generate whole NgRx state features
  2. Recognize and leverage the NgRx 80/20 Rule
  3. When in doubt, pull your logic one level up (to preserve nice clean one way dependency graph)
  4. Create perfect view selector view$ ( or viewModel$ ) for your container components
  5. Always use RouterStore and its selectors to access router state
  6. Always name (and implement) your actions as “events” instead of “commands”
  7. Embrace local selectors and actions specific to given container ( or interceptor, feature, … ) which makes implementing of other best practices a breeze
  8. Effects are for orchestration only, make sure that the actual logic is in services to keep the effects clean
  9. Effects can be triggered by ANY RxJs stream
  10. Get comfortable using Redux DevTools and prevent potential performance issues as your application state grows

NgRx General Tips

Use schematics to generate whole NgRx state features in one go

Schematics are one of the best things about using Angular to create our frontend applications!

Angular Schematics allow us to generate new workspace, application, architecture and every component, pipe or service with ease and hence not feeling any pain with regards to Angular’s explicit (some would say verbose) approach to building applications.

For example, running ng g m features/some-feature --route some-feature --module app in our terminal will create a whole new lazy loaded feature for our application which will be integrated in to the route configuration and comes with a container component out of the box. Yes, it can be that easy!

NgRx comes with its own schematics package called @ngrx/schematics which allows us to setup whole NgRx state features by running a single command.

Lets imagine we want to add new product feature (page) to our application. We can get a whole setup just by running two commands:

ng g m features/project --route project --module app

ng g @ngrx/schematics:feature features/project/state/project -a -c --module features/project/project

Example of files generated by running Angular Schematics to generate lazy loaded module and a NgRx state feature for that module

Besides obvious advantage like not spending time copying (or writing) the files by ourselves there is an additional benefit of consistency!

Consistency in naming, code style, patterns and general approach allows us to be effective when working on multiple features (or applications) from the get go and is one of the best things about Angular and NgRx based development!

🤔 Consideration: Sometimes, we might need more than one state module per feature as feature manages state of multiple entities. In that case the above mentioned schematics can be adjsuted to generate state-<entity-name>/ folder instead of plain state/ folder.

That way our feature will stay clean and easy to understand, plus it will become easy to move our state slice around, for example in case we need to pull it one level up to the core/ so that it can be reused by more than one lazy feature of our application as will be discussed in dedicated section of this article!

Recognize and leverage the NgRx 80/20 Rule

With more than 2 million downloads per month, it should be pretty safe to state that NgRx is used by lots of folks out there!

The amount of users makes it that much more surprising that I have never seen it discussed online, the NgRx 80/20 rule!

The MOST GENIUS things about NgRx is that whole 80% of logic we have to write is just plain TypeScript functions which are NOT aware of Angular or RxJs

That’s right, the majority of NgRx implementation is as simple as it gets — just pure functions *— which are the simplest and easiest to understand building blocks of every programming language!

NgRx architecture diagram showing that only NgRx Effects which are Angular services themselves are aware of Angular (dependency injection) and RxJs Observable streams

Let’s dissect this diagram further. The concepts implemented as data structure or pure functions* are:

  • store — just an interface and initial state data structure
  • reducers — pure functions
  • selectors — pure functions*
  • actions —data structures/pure functions*

* action creators and selectors could also be impure, for example when using something like uuid to generate the payload but the purity claim will hold for most practical purposes 😉

  • effects — Angular service (dependency injection), RxJs streams

On the other hand, last 20%, the NgRx effects are the only place aware of Angular as the NgRx effects themselves are implemented as Angular services so they can inject other services (store, http client, custom business logic services or anything else really…) and also implement logic with help of RxJs Observable streams.

In my experience, RxJs usually represents the most challenging part of the Angular application logic so it is important to point out that the NgRx Effects come with their own toned down / tamed version of RxJs streams:

  • we do NOT have to manage subscription lifecycle as NgRx effects are subscribed by the NgRx when the effect is initialized
  • streams are broken down into small chunks which are easier to understand, fix and evolve
  • small effects can be composed effectively, eg effect A triggers B1 and B2, once both of them are done, the C is triggered (more on that in the dedicated effects orchestration part of this article)

The Benefits of NgRx 80/20 Rule

As we have discussed, up to 80% of NgRx based application code is implemented using plain data structures and pure functions which is great news as it makes the implementation and especially testing a very straight forward business!

Let’s consider following example s— even though they might not seem the same on the surface — there is fundamentally no difference between something like function multiply and a reducer or a selector…

Example of a very simple pure function — multiply, receive two arguments and return single result, will always return the same result for the same arguments

Excuse the following huge selector, you can ignore its implementation, the point is even though is large, it’s basically the same as the previous multiply function on the philosophical level (even though it could probably be further broken down in smaller parts for even easier testability and reusability…)

Example of a large NgRx selector, an easy to test pure function

Such selector, even though contains a lot of implementation can be tested in a very simple fashion…

Example of the NgRx selector test, at its core its just a call to pure JavaScript function

The same principles would apply to NgRx reducers and custom action creators which brings us to a final conclusion of NgRx 80/20 rule…

NgRx is great because it allows us to implement as much logic as possible (80%) in the form of simple pure functions which are easy to understand and test!

NgRx while purely RxJs stream based (selectors are consumed as RxJs streams in the component template or NgRx effects) cleverly abstracts those RxJs streams away so that we do not have to struggle to avoid all the pitfalls and complexity of dealing with such streams, and that’s the genius of its architecture, thank you @ngrx team!

One Way Dependency Graph -When in doubt, pull your logic one level up

Hopefully our Angular application is following simple scalable architecture like one in the following image…

Example of Angular application architecture with eager core and lazy features (which import shared module which provides simple reusable components, directives and pipes)

If not, I highly recommend you to take a detour and check one of my previous articles which dives deep on advantages of such architecture and how to implement it 😉

How to architect epic Angular app in less than 10 minutes! ⏱️😅In this article we are going to learn how to scaffold new Angular application with clean, maintainable and extendable…tomastrajan.medium.com

As we have aggreged on the overall architecture, it allows us to take a closer look on its implications for NgRx state features (modules / state slices)

♻️A small recapitulation: In general, when we speak about NgRx state feature (module / state slice) we mean files generated by the @ngrx/schematics , especially the feature schematic which generates the whole state feature.

Example of a NgRx state feature generated by the NgRx feature schematics which includes whole NgRx setup

Now, when we develop application it is common to start with single state slice per lazy feature.

As the requirements become more clear or are expanded, it is often the case that we need to access NgRx state slice from one lazy feature in another lazy feature of our Angular application…

⚠️ Importing stuff between sibling lazy features is forbidden because it would break all the benefits of such architecture and could in theory lead also to runtime errors based on order in which user navigated to individual lazy features!

Because of these reasons, the only way to fulfill our requirement is to either:

  • extract whole state feature into core/ which then can be imported by any lazy feature while preserving clean one way dependency graph
  • split state feature of the lazy feature into two — reusable state will be extracted in to fresh new state slice implemented in core/ while the lazy feature specific state will stay in that feature
Example of extracting part of the lazy feature state into core to make it available in whole application

The described application architecture is fractal, which means it can grow infinitely — at least in theory 😅

In practice this means our lazy features can have lazy SUB-features which can have lazy SUB-SUB-features, …

The same rules will apply in those cases, if we had a SUB-feature-A state which we need to access in SUB-feature-B then we have to pull that state slice one level up into parent lazy feature (so not core/, always just one level up)

Example of a fractal architecture, let’s hope you don’t need to implement so many lazy features 😅🤣

Follow me on Twitter because you will get notified about new Angular blog posts and cool frontend stuff!😉

NgRx Selectors

Selectors are pure functions used for obtaining slices of store state. More so, selectors are perfect place to create all your derived state. In practice, they are just pure functions without any notion of Angular or RxJs streams so they are naturally easy to write, test and understand!

Create perfect view$ (view model) selector for your container components

One of the best properties of NgRx is that it allows us to minimize implementation of our components which are coincidentally also hardest part of Angular to test, just amazing!

How does NgRx supports us in this case?

Well, our components implementation can be reduced to two concepts

  • retrieve and display state from the store using NgRx selectors
  • dispatch actions based on user interactions (yes, that actually rhymes)

While this solution is really amazing, especially compared to our standard logic riddled Angular component, developers often choose an approach where they keep adding more and more selectors to the component itself…

Example of Angular component which uses NgRx selectors and store to dispatch actions. Unfortunately, component still retrieves state from many selectors which leads to more complicated template and often multiple subscriptions to the same streams!

Over time we might end up with many selectors from various state slices. All those independent selectors must be subscribed one by one in the template using the | async pipe. Even worse, we often end up with multiple subscriptions per selector in a single template!

Let’s improve the status quo by introducing dedicated view$ selector for our container component.

The component will use single dedicated “”selector which will deliver tailor made view state which has the perfect shape for the components template so it can be rendered with ease!

Example of Angular container component which uses dedicated “view” NgRx selector which delivers view state with “perfect” shape for the component template to be rendered

Using dedicated view$ selector comes usually hand in hand with wrapping of the whole components template with <ng-container *ngIf="view$ | async as v">.

Once we subscribed to the view$ and unwrapped its value in the local template v variable we can start using it to access any state properties such as v.tasks or v.loading!

Please consider that using *ngIf doesn’t represent any problem with regards to not seeing any content in the beginning (for example, before we loaded data). Subscribing to selector with | async pipe is resolved immediately and the selector will always provide initial value which is at least the initial state from the store.

This goes hand in hand with having explicit loading and error states in our store state slices to show relevant user feedback!

🤔 Consideration: Some developers prefer to store reference to the view selector in the viewModel$ and vm respectively. Remember, it doesn’t really matter how you call it as long as you implement the concept of dedicated “view” selector for our container components 👍

Another benefit of this approach that if further simplifies testing because we can just mock what this selector deliver for the component view which becomes decoupled from what other selectors are delivering.

Always use RouterStore and its selectors to access routing state

NgRx comes with a nice little package called @ngrx/router-store …

Bindings to connect the Angular Router with Store. During each router navigation cycle, multiple actions are dispatched that allow you to listen for changes in the router’s state. You can then select data from the state of the router to provide additional information to your application — Official NgRx Docs

As highlighted in the quote above, the @ngrx/router-store allows us to access Angular Router state using selectors which is extremely convenient and preferable compared to default way of doing things. Consider the following example…

Example of a ad hoc “bridge logic” between Angular Router (especially ActivatedRoute API) and NgRx

We need to get selected dashboard ID from the URL and the standard way to do that without @ngrx/router-store would be to inject ActivatedRoute , then retrieve the ID and dispatch appropriate action while manually managing subscription state, there must be a better way!

Imagine a world where you can just…

Logic-less component, a common positive occurrence in NgRx codebases, don’t forget that the components are the hardest and slowest to test!

And retrieve desired dashboardId from URL (path param) using a selector in nice dedicated effect…

Example of NgRx selector and effect implementation to retrieve dashboard Id from URL (path param) and use it to select dashboard in the application state by dashboard ID

The key part here is the selectRouteParam selector factory provided by the @ngrx/router-store , check out the list of all selectors and selector factories available out of the box!

List of all selectors and selector factories available out of the box when using @ngrx/router-store

Do you enjoy provided content and think that your team or organization could benefit from the know how? My good friend Kevin Kreuzer and I are offering wide range of Angular consulting services, check it out!

Welcome to the world of Angular excellence — angularexperts.io

Empower your team with proven real world expertise to get your projects up and running in no time! Explore our extended offer ranging from workshops, kickstarting your projects to ad-hoc consulting to solve a pressing issue — Get in touch with Angular Experts

NgRx Actions

Actions are one of the main building blocks in NgRx. Actions express unique events that happen throughout your application. Everything from user interaction with the page, external interaction through network requests, and direct interaction with device APIs, these and more events are described with actions — NgRx Docs

Name your Actions as “Events” instead of “Commands”

Let’s consider the following effect which should reflect userId into the query params of the URL.

NgRx effect which reflects userId into the URL as a query parameter

There is nothing inherently wrong with the effect itself as it will do its job just fine… Nevertheless, there is still a room for some improvements!

Imagine a situation where a large application might contain a couple of unrelated features which all need to be able to set test user into the query params of the URL.

In that case, we wont be able to really understand what is going on just by observing the list of actions as they happened in the “application history” or more precisely, the actions log of the Redux Dev Tools (browser extension)

Let’s improve our implementation by adjusting the name of the action and the effect itself!

NgRx effect which reflects userId into the URL as a query parameter with better naming and multiple trigger actions to improve readability of the “application history”
  1. We renamed the effect itself from changeTestUser to reflectTestUserIdIntoQueryParams which is much more descriptive
  2. We split action into two (or potentially more) actions based on the action origin
  3. We renamed actions to follow “event” pattern ( what happened? testUserSelected ) instead of “command” pattern ( eg changeTestUser )

Great, our effect now reads like a sentence!

Reflect test user ID into query params (when) test user was selected (in the toolbar) or (when) test user was selected (as a part of some business flow)

Now that we established that effect describe events which happened in our application, the second point will make much more sense.

The actions should be named in a way which focuses their source (origin) and NOT to their destination (reducer / effect)!

Consider the changeTestUser action which is basically a command which implies it belongs to a “destination” where it will be performed by some logic (either reducer or effect or both).

Such action then might be dispatched from multiple parts of application and we would have no way of knowing what happened just by viewing the application action history in the Redux DevTools…

Now if we name it correctly like an event, for example testUserSelected, we might create many such actions with different origins:

  • [Toolbar] Test User Selected
  • [Test User Selector Widget] Test User Selected
  • [Some Business Flow] Run As Test User Selected

Naming actions as events gives us much better insight into what is going on in our application!

Embrace local selectors and actions specific to given container, widget or interceptor…

In practice, not every selectors or actions file belongs to a generated state (feature) module (eager or lazy state feature which we would get when running ng g @ngrx/schematics:feature schematics)

In those cases, it is more than alright to create a dedicated selectors or actions files which is co-located next to it consumer. Consider the following example of auth-interceptor which needs to retrieve state from multiple state slices and because of that we are going to introduce a dedicated selector file.

Example of a dedicated local NgRx selector to provide state for the auth-interceptor by combining data that is made available by selectors exposed by other eager NgRx state modules (slices / features )
Example of a NgRx local selector implementation which combines data exposed by the selectors of other eager state slices

NgRx Effects

NgRx allows us to implement sideeffects as a well defined separate concept with first class support from the library itself!

First class sideeffects support sets NgRx apart from the other Angular state management libraries which support it partially or not at all…

Effects are for Orchestration

NgRx effects are dedicated place to implement all the asynchronous flows, processing and any side effects in general…

It’s really great that NgRx prescribes dedicated concept for such type of logic which leads to consistent clean implementation across wide range of application features and applications themselves.

⚠️ While implementing NgRx effects, it might be temping to skip creation of a dedicated feature service and implement all the logic, requests, transformation and what ever have you directly in the effect itself — do NOT do that!

NgRx effects should be as lightweight as possible and the real business logic implementation should be handled by dedicated services

More so, even though is possible to implement complex asynchronous orchestration as a single effect — which means it’s gonna be a complex RxJs stream — it is MUCH better to split it into multiple smaller effects.

Every successful effect is then going to trigger the next one with its success action. This allows also for branching out for multiple parallel processes and eventually collecting all the results for following serial processing steps.

Example of NgRx effect logic which could have been implemented as a single effect but is much more manageable and easier to understand when split into multiple smaller standalone effects

Effects can be triggered by ANY RxJs Observable stream

Most of our NgRx effects are triggered by the stream of actions$. This occurrence is underlined by the fact that when we generate NgRx state feature using @ngrx/schematics the generated effect contains exactly such snippet as an example implementation.

Example of a NgRx effect generated as a part of NgRx feature generated by @ngrx/schematics, the effect is triggered by the stream of all actions$ out of the box

The thing about the effects is that they can be triggered by ANY RxJs Observable stream. This can help us solve many common use cases like:

  • triggering of some periodic processing with given interval (eg refreshing of an auth token, uploading of logs to backend, prompting user to take action before she will be logged out, …)
  • reacting to some user interaction (eg stream of scroll events to trigger loading of more data as the user scrolls to the bottom of the page, …)
Example of a NgRx Effect triggered by user interaction when user scrolls to the bottom of a page
  • reacting to the store state changes using NgRx selector as an effect trigger (eg show / hide notification popup based on the amount of notifications in the store, perform something when query params change)
Example of a NgRx Effect triggered by the NgRx selector stream instead of more common stream of actions$

Get comfortable using Redux DevTools

The one of the most important best practices discussed previously is the Name your Actions as “Events” instead of “Commands”. As we apply it to our codebase we will discover an amazing side-effect (not in NgRx sense 😉) that our application history will become much more useful tool for understanding what is going on and what went wrong in case we encounter some buggy behavior.

Make sure that you install and familiarize yourself with the Redux DevTools!

Redux DevToolsRedux DevTools for debugging application’s state changes.chrome.google.com

The Redux DevTools were originally created to provide nice application history and state overview for the apps using Redux state management library.

NgRx implements the same patterns, just in a way that plays really nice with Angular as the host framework. That makes them a natural fit.

NgRx / Redux DevTools integration is implemented in the @ngrx/store-devtools package. It’s definitely a good idea to add this at least in development mode.

As our application and especially managed state grows, we might encounter a new problem…

The state instrumentation for the Redux DevTools basically has to serialize whole application state, for every action that has happened.

As we can imagine, it will get out of hand pretty quickly if our state is large and our application dispatches actions with high frequency. The serialization itself becomes slow (or even grinds to halt) while the memory consumption will be sky high!

This is definitely NOT what we want but luckily there is a solution. The @ngrx/store-devtools allows us to provide custom implementation for the actionSanitizer and the stateSanitizer .

Provide custom action and state saniziter for NgRx StoreDevtoolsModule

In practice I found it very useful to remove the whole payload from <LoadSomeData>Success actions as that state can be viewed in the network tab of the browser dev tools so it is not necessary to duplicate it in actions for debugging purposes.

Example of a NgRx actions sanitizer which will strip any payload from “*Success” actions to improve Redux DevTools performance

As for the state sanitizer, it will depend on particular application so it’s hard to provide generally applicable guidelines. One example of state sanitizer could be truncating of a long list of large items to let’s say only 5 items…

Example of NgRx store state with truncated list of items to prevent performance issue, its a custom implementation so it will be specific to given application and application state

As of 2022, the Angular + NgRx represents THE BEST COMBO* for high productivity which enables you develop complex and robust frontend applications with clean architecture which can scale to support 10s of features with ease, from solo developers to whole enterprise organizations with many teams and developers!

* if we can assume general competence with both technologies

And that’s it for today! 🔥

I hope You enjoyed learning about real world proven NgRx best practices and what benefits You can reap by implementing these ideas in your projects!

Please support this guide with your 👏👏👏 to help it spread to a wider audience, it would be very appreciated and don’t forget to share it with your collogues who could find these tips useful🙏.

Also, don’t hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan

And never forget, future is bright

Obviously the bright future! (📷 by André Filipe )

Wanna learn some Angular and You and Your team are from Switzerland or surrounding countries (in person) or worldwide (remote) ? Check out my workshop offer!

In person & remote Angular workshops for you and your team!

Working in large polyrepo enterprise environment ?

Check out OMNIBOARD — the best tool for software architects & engineers that helps them to understand and evolve polyrepo environments of their enterprise organization

OMNIBOARD allows you to analyze all your project repositories and create dynamic dashboards to understand your environment, track progress and drive change

Other Artices

See all articles

Get the latest from Coalist

Share your email so coalist can send you guides and industry news.

    By clicking this button, you agree to our Terms of Service and Privacy Policy.