This post is a step-by-step guide for setting up your Ngrx Development environment, namely the Ngrx DevTools, but not only: we will also talk about some best practices for developing Ngrx applications in general.
These practical tips will likely make a huge difference in your Ngrx development experience (if you haven't implemented them already).
Any Ngrx project would benefit from having these tips in place in order to make the most out of the DevTools, and of the store architecture in general.
Table of Contents
In this post, we will cover the following topics:
- What are the Ngrx DevTools?
- The main features of the Ngrx DevTools
- What are the benefits of the Ngrx DevTools?
- Installing the Ngrx DevTools with Ngrx Schematics
- What is the Ngrx Router Store module, why do we need it?
- Installing the Ngrx Router Store module (ngrx-router)
- Using a custom router serializer in order to avoid freezing the DevTools
- Preventing several types of bugs with Ngrx Store Freeze
- Action conventions and best practices, to help to make the most out of the DevTools and of the store architecture in general
So without further ado, let's get started with our Ngrx DevTools deep dive!
What are the Ngrx DevTools?
The Ngrx DevTools are a Chrome / Firefox browser extension that includes a UI for inspecting and interacting with a Ngrx-based application.
As an example, here is a screenshot of the Ngrx DevTools in action:
The main features of the Ngrx DevTools
As we can see, inside the Ngrx DevTools we have:
an Action Log, that gives us a great understanding of how the application works, and what parts of the application are triggering which Actions
A State inspector, that allows us to easily inspect the in-memory store state
a Time-travelling debugger (the Play button and timeline at the bottom), that allows us to replay any Action at any given point of the debugging session, and even replay the whole session while navigating through multiple screens
What are the benefits of the Ngrx DevTools?
Here are some of the benefits of the Ngrx DevTools:
we can visually see the content of the store at any moment, which is essential for debugging
we can have a new developer on the team inspect the application with the DevTools, and have it get a good initial understanding of how the application works
if we manage to get the client state of users in production, we can use the DevTools to reproduce bugs locally, just by importing the user production state
The key benefit of the DevTools is that it gives us some immediate visual feedback about what the application is doing at all times, making it much easier to understand what is going on.
Installing the Ngrx DevTools with Ngrx Schematics
It's best to setup the Ngrx DevTools from the very beginning of the project. We can setup an Ngrx Store and configure the DevTools all in one go with the following Angular CLI command:
ng generate store AppState --root --module app.module.ts
In order for this command to work, we will need first to enable Ngrx Schematics, by adding this Angular CLI configuration:
ng config cli.defaultCollection @ngrx/schematics
After running these commands, we will see that the Ngrx DevTools are enabled in the root application module, but only if the Angular CLI is running in development mode:
And with this, our application now supports the Ngrx DevTools! Now we just have to install the DevTools extension in our browser, by following these instructions.
After installing the extension, you now should have the Ngrx DevTools available under the "Redux" menu option of your browser development tools (open them with Ctrl+Shift+I in Chrome).
After opening the Ngrx DevTools, you will have to reload the application in order to start debugging the application.
Setting up the Router integration (ngrx-router) from the beginning
With the Ngrx DevTools, having the browser extension up and running is only half the story. As soon as we start using the DevTools, we will run into scenarios where the time-traveling feature comes in handy.
But the time-traveling debugger by default cannot navigate through multiple application screens, so we can't use it to effectively replay the complete user UI session from the beginning.
In order to enable full time-traveling debugging, we need to somehow integrate the DevTools with the Angular Router, so that going back in the Action timeline also means navigating to previous screens.
The Ngrx Router Store module allows us to do exactly that, so let's go ahead and enable it in our root application module.
Installing the Ngrx Router Store module
In order to enable the Ngrx Store router integration, we need to first declare the following in the root module imports section:
This configuration means that the Router Store module will save its state inside the store under an application state property named
router (configured via the
Setting up the Router Store reducer
In order to populate the store with this new router state, we will need a new reducer for handling all state under the
router property. We can configure this reducer in our root reducer map in the following way:
And with this, we have finished the setup of the Ngrx Router Store module! Let's now have a look at how the Router Store integration works in practice.
The Ngrx Router Store in Action
Now as we restart our DevTools, we will see that router navigation occurrences now show up as actions in the Action Log:
If we inspect the content of the action logged as
ROUTER_NAVIGATION using the Ngrx DevTools, we see that it contains all the information necessary for performing a router transition:
Also, if we inspect the state of the store, we will see that there is now some new state under the
As we can see, now with each router navigation the Ngrx Router Store module is capturing the router state and saving it inside the store, under the
This will allow us the replay all the actions of the user from the beginning of the debugging session, including router navigations.
How important is the Ngrx Router Store module, is it optional?
It might look like the Ngrx Router Store module is optional.
But if you really want to be able to use the Ngrx DevTools in a lot of practical scenarios where often router navigation is involved, then this module ends up becoming essential.
Note that although this module would potentially also allow us to trigger router navigation by dispatching store actions (see here to learn how), that is not the main goal of the module.
The main goal of the Router Store module is not to replace the Angular Router navigation API.
Instead, its main goal is to enable the DevTools and time-traveling debugging to work well in a much larger number of scenarios where routing is involved, although having the router state in the store might also come in handy in other situations.
Using a custom router serializer in order to avoid freezing the DevTools
If you are using an Ngrx version earlier than this one, you will have likely noticed that the Ngrx DevTools might crash due to unresponsiveness problems.
This was caused due to problems in attempting to serialize the Angular Router state, which by default contains cycles in its object graph.
This means that in order to solve this issue and have fully functioning Ngrx DevTools, you might have to install a custom Router state serializer, that stores the router state in a format without graph cycles.
This unresponsiveness/crash problem is solved in newer releases, but if you still have it in your application then its better to quickly install a custom router serializer, in just a few steps.
Here are the instructions on how to install the custom router serializer.
Note: for earlier Ngrx releases that had this DevTools unresponsiveness issue, using a custom router state serializer was usually not optional in practice
With this last problem solved, you should now have fully functional Ngrx Development tools, with full time traveling debugging capability!
Prevent several types of bugs by using Ngrx Store Freeze
Let's now cover another very useful development tool that we have available in the Ngrx ecosystem: Ngrx Store Freeze.
As you know, the store reducer functions need to be written in a very precise way, and not following that well-known set of rules can be a recipe for some very hard to troubleshoot bugs.
One of those rules is that reducer functions should be pure functions, that take as input the current state and the action, and return the new state.
The reducer function is not expected to any way mutate either the existing state or the dispatched action. Instead, its expected that the reducer function returns a new version of the state without mutating any of its inputs.
Not doing so so might potentially cause some very hard to troubleshoot issues in our application, especially if we are using OnPush change detection in large parts of our component tree.
Other than that, it also breaks the time-traveling debugger feature.
Mutating the store state at the component level
Another common source of hard to debug errors while building a store application is the possibility of some component in our component tree to accidentally get a direct reference and directly mutate the store state.
Having a direct reference to the store mutable state would allow the component to directly mutate the store state and therefore break the store pattern, instead of having to dispatch an action in order to change the store state.
Mutating the store state directly either via the component tree or inside reducers also breaks the time-traveling feature of the DevTools.
This, combined with the possibility of introducing time-consuming bugs and architecture errors makes this problem something that we want to tackle from the very beginning.
As we will see, Ngrx Store Freeze provides us with a really simple solution for this very common set of potential problems.
How does Ngrx Store Freeze work?
Ngrx Store Freeze is easy to install and provides an effective solution to all the previously mentioned problems related to store state mutability.
The Ngrx Store Freeze module automatically deep freezes our full store state object as well as dispatched actions. It does so by going to each object property of the store state and setting it to read-only.
Nested properties are also recursively frozen, meaning that the whole store state object is made effectively immutable.
With this setup, it's now impossible for reducer functions to mutate the store state or the action, both in our reducers and in our component tree.
Installing Ngrx Store Freeze
Ngrx Store Freeze is a meta-reducer, meaning that its just a normal reducer function. The difference towards a normal reducer is that a meta reducer is applied on top of the output of another reducer function.
Meta-reducers can then be combined in an ordered chain, with each meta-reducer building on top of the output of the previous meta-reducer.
The Ngrx Store Freeze meta-reducer will be applied after all the normal reducers have been triggered for a given action, and it will freeze the whole store state before the state can even be sent back to the component tree.
Here is how we install the Ngrx Store Freeze meta-reducer:
Notice that Ngrx Store Freeze is only active in development mode. In production, we won't get the potential performance penalty linked to deep freezing the store state with each action dispatch.
Notice that this performance hit is only noticeable for a very large amount of store state
Ngrx Store Freeze in Action
Let's then go ahead and see how Ngrx Store Freeze works! Here is an example of an incorrectly written reducer, that accidentally mutates the original store state:
As we can see, the reducer for the login action is directly mutating the original authentication feature state.
Without Ngrx Store Freeze, this might not cause any immediately visible issue, other than breaking the time-traveling debugger feature of the NGrx DevTools.
But if our application is using OnPush, this would likely already start causing view synchronization issues, where the view and the store data are no longer in sync.
Catching mutability issues from the start
In order to avoid this problem altogether, we just have to active Ngrx Store Freeze. This time around, we would get the following error in the console:
store-devtools.es5.js:423 TypeError: Cannot assign to read-only property 'loggedIn' of object '[object Object]' at authReducer (auth.reducer.ts:22) at store.es5.js:172 ...
The error might look at bit intimidating at first, but looking further into the stack trace, its actually a very useful error message: notice that on the stack trace we even have the exact line of the reducer function that is causing the issue.
By refactoring the reducer in order to return a new state object instead of mutating the existing one, the problem is now fixed:
Preventing store state mutation at the component level
Note that Ngrx Store Freeze also prevents issues caused by attempting to mutate the store state directly at the component level, which would break the store architecture in general as well as the DevTools time-traveling functionality.
If you don't have Ngrx Store Freeze in your Ngrx application, you might want to give it a go and see if you can preventively catch a couple of potential bugs in your application!
The best is to use it from the beginning and avoid altogether all these potential issues. Besides, ensuring store state immutability makes it really simple to adopt OnPush change detection everywhere in the application (if needed), which depending on the application can make for a nice UI performance boost.
Action Conventions, make the most out of the DevTools and the Store architecture
To make the most out of the Ngrx DevTools and the store architecture, it's important to name our actions and choose their action type description string according to certain useful conventions.
Going back to our action log, notice how we can already tell something about the application just by reading the log:
Without looking at any source code, we can already tell that:
- the user logged in
- then the user navigated to the Home Page
- There, a list of courses was requested
- Eventually, the list was loaded from the backend using an API request
This log is readable because the action types of this application are written in the following way:
These action types follow the following convention:
The convention works like this:
The Source is the part of the application that triggered the Action. For example, the screen that dispatched the Action
The Event is the application event linked to the Action
You can learn a lot more about Store architecture best practices in general and about the Source/Event convention in particular by watching the following awesome talk by Mike Ryan "Good Action Hygiene with Ngrx":
Important takeaways from the Source /Event Action convention
This convention means a couple things regarding the way that we look at Actions.
The first thing is that Actions are meant to be specific to a given screen or Effect, and not generic. For Example, the Action Source "Course Home Page" is very specific to a given component of the application.
This also means that in order to have readable action logs, we should avoid reusing Actions between screens.
Instead, if the Actions share the same reducer logic, we can use the switch block fall-through feature in our reducers to apply the same reducing logic to multiple actions.
Action Design: Events instead of Commands
We should avoid designing our Actions as Commands: we should make them Events instead. The difference is subtle but critical in terms of long-term application maintainability.
Making an Action an Event means that an Action reports something that has already happened in the near past, and that is well known in the scope of a given component or effect.
This last point is especially important because this way the component dispatching the Action cannot fall into the pitfall of becoming aware of other parts of the Application.
The dispatching component simply dispatches an Event under the form of an Action, and the store (which includes the Effects) will decide what to do in response to that: call a backend, run the reducers, both, etc.
Why not make an action a Command?
If we would make the action a Command, we would be making the component decide what the store or even what other components should do in response to the Action, indirectly via the store.
One of the main goals of the store pattern is to remove from the components the ability to directly modify application state, instead only the store can do that. The components simply subscribe to the state without modifying it, project it into the view, and report events back to the store.
The components that get data from the store are meant to be kept well isolated and unaware of each other, and Command-like actions like
IncrementTopMenuCounter dispatched in multiple parts of the application would not allow that.
One of the best ways to make the most out of the Ngrx store architecture is to have the Ngrx DevTools fully up and running from the very beginning of the project.
And to have the complete time-traveling functionality up and running, we really need the Router Store integration. Depending on the Ngrx release, a custom Router state serializer is going to be essential as well.
With this, we will have solid Ngrx DevTools for the whole duration of the project, so taking a moment to get them running at the beginning is well worth it.
Also, in order to avoid common mutability-related bugs and allow a simplified switch to OnPush, it's also important to ensure store state immutability.
The simplest way to do that without adopting something like ImmutableJs is to simply use Ngrx Store Freeze in development mode. This helps a lot ensuring that our reducers are correctly written, as well as preventing our components from accidentally mutating the store state.
Finally, but probably the most important, to make the most out of the Store architecture and the Ngrx DevTools it's important to learn about Action design best practices.
More on Ngrx
To learn more about Ngrx in general, have a look at this great batch of ng-conf talks. I hope that this post helps with getting started with the Ngrx DevTools, and that you enjoyed it!
If you have some questions or comments please let me know in the comments below and I will get back to you.
To get notified of upcoming posts on Ngrx and other Angular topics, I invite you to subscribe to our newsletter:
Video Lessons Available on YouTube
Have a look at the Angular University Youtube channel, we publish about 25% to a third of our video tutorials there, new videos are published all the time.
Subscribe to get new video tutorials: