Over the past years, TypeScript has become more powerful. Especially when it comes to its type system, in other words, how smart Typescript is. With every new release, there's always one or more features that make it easier to write better code.
This is something that NgRx leverages to its full potential. NgRx uses new TypeScript features to build type-safe state management libraries, which makes NgRx easier to use and shortens the feedback loop to detect problems earlier.
Sadly, keeping up with everything is time-consuming, and a lot of examples are outdated. I hope that this post helps you to get to discover (and use) the latest APIs that NgRx has to offer.
Most of these tips and tricks are available as rules in the NgRx ESLint Plugin. Some updated APIs also have an automatic migration that is run via the
ng-updatecommand, other might have an ESLint fixer to refactor the code to the (newer) recommended syntax.
Let's start off at the base of NgRx, Actions.
You can take a look at the documentation to learn more about Actions.
We can summarize this section into one sentence, stop defining actions as classes.
While it's still supported, NgRx has a more robust way to define actions.
Previously, the above snippet was the way to define actions in NgRx because it was the only way to keep reducers type-safe (more on this in reducers).
In the newer version(s) of NgRx (v8+), we can now use the
createAction method to define actions.
This way is less error-prone, and is also automatically type-safe.
The equivalent of the above snippet can be refactored to the following actions.
Also, it's important to notice that we aren't required to define the
CustomerPageActions union type anymore.
Since NgRx v14, it's possible to group actions that share the same source with
The only caveat is that you can't use your IDE refactoring tools to rename the action or to find all its occurrences, but on the other hand, this shouldn't be a problem.
We can detect where an action is dispatched by looking at its source, and renaming an action (event) should be a rare occasion - I would prefer to create a new action in this case because it mostly means that it's another unique event.
You can take a look at the documentation to learn more about Reducers.
We've seen how to create actions, let's take a look at how these are consumed in reducers.
The reducer is a "simple" function that receives the current state, and the dispatched action. Within the reducer, there's a switch statement to handle the proper state updates.
To make the reducer type safe the dispatched action needs to be annotated with the action union type. When a reducer handles actions from different sources, all union types need to imported.
Within a case block,
action has the correct type.
It's very important to add a default case to return the state as-is for the actions that aren't consumed by the reducer.
If you don't do this, the state is cleared (state will be
In other words, to make the reducer type-safe we have to manually create additional types.
From NgRx v8, this isn't the case anymore.
There's a better way to define reducers compared to the reducer method, which is
Instead of the reducer method with switch cases, we now have:
createReducerto replace the reducer method
onmethod to listen for one or multiple action(s), replacing a switch case
Again, the main benefit of this approach is that it's more robust and type-safe by default (without additional steps).
The refactored version using
createReducer looks like this.
The same reducer, but with actions created with
createActionGroup uses the
CustomerPageActions group instead of the loose actions.
The only downside is that
createReducer doesn't support a default case.
To take it a step further, we can use the
createFeature method, which is introduced in NgRx v12.1.
With it, the reducer is wrapped in a feature (more on this in Registering the Store).
But more important, a default set of selectors are generated based on the state properties.
You can take a look at the documentation to learn more about Selectors.
Selectors remained the same throughout the NgRx versions, so there's not much to say about them. The only tip that I want to mention is that some part of the state can be persisted in the URL and shouldn't be duplicated in the store. Instead, you can make use of the selectors provided by @ngrx/router-store.
Instead of creating a global application state,
AppState in most cases, I prefer to not have a global state defined.
This has several benefits. The biggest one is that we can remove a layer of complexity, and keep all state levels equal.
Combining multiple slices of the state into a single object, being as a global state or feature state, is also a common source where things can get wrong (especially when using the older syntaxes we've covered above).
So why not avoid this complexity and keep things consistent?
To achieve this, register the global store in the root module, without providing a state. Then, register each slice as a separate feature.
We can then re-use the same pattern in the feature modules.
In NgRx v15, there's a new way to register the Store when you're using the standalone components API.
provideStore you can register the NgRx store (and register the root state), and with
provideState you can provide feature states.
And in a feature:
You can take a look at the documentation to learn more about Effects.
Even if it's deprecated since NgRx v11, I still see projects that use the
@Effectdecorator is going to be removed in NgRx v15.
This code snippet has multiple shortcomings.
First, an Effect should always return an action, using
@Effect doesn't make sure of this.
Second, to have an effect that knows the signature of the handled action, either the
Actions needs to be typed by providing the action union generic, or the
ofType operator needs to use a generic of the handled action.
But, of course, we can do better with the updated API.
The above code can be refactored to use the
This solves both shortcomings of
@Effect as it ensures that the effect returns an action (instead of all types), and the action is also automatically typed.
Notice that we don't need to provide a type to the injected
Actions, nor to
In NgRx v15, there's a new way to register your Effects when you're using the standalone components API.
EffectsModule.forFeature(), there is the new
provideEffects method that can be used to register your root and feature effects.
And in a feature:
To select a slice of the state, or to dispatch an action, the
Store instance needs to be injected.
In contrast to the first NgRx versions, when it was required to provide a state generic to the
Store, this has become optional in NgRx v9.
If you're using selectors (which you should be), adding a generic only raises confusion and thus is considered as a bad practice.
Instead, simply inject the
Store without a generic.
By doing this, you won't lose the type-safety NgRx provides, as the selectors provide the type-safety that is needed.
It's important to know that not everything needs to be in the global store. Sometimes it's better to keep the state local to the component. This is where the Component Store, @ngrx/component-store, comes in. For a comparison between the Component Store and the Global Store, and when to use which, take a look at the documentation.
Since the introduction of component-store in NgRx v9.2 not a lot has changed about its API, but the existing API have had a couple of small improvements.
The biggest improvement in NgRx v15, is that selectors can easily be combined into a model.
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.