Tim DeschryverTim

You should take advantage of the improved NgRx APIs

The NgRx logoThe Angular logo
@tim_deschryver

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-update command, 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.

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 createActionGroup. 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.

Reducers

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 undefined).

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 createReducer.

Instead of the reducer method with switch cases, we now have:

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.

Selectors

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.

Registering the 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. With provideStore you can register the NgRx store (and register the root state), and with provideState you can provide feature states.

And in a feature:

Effects

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 @Effect decorator.

Heads-up: the @Effect decorator 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 createEffect method. 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 ofType operator.

In NgRx v15, there's a new way to register your Effects when you're using the standalone components API. Instead of EffectsModule.forRoot([]) and EffectsModule.forFeature([]), there is the new provideEffects method that can be used to register your root and feature effects.

And in a feature:

Injecting the Store

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.

Component Store

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.

Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo
Support the blog Share on Twitter Discuss on Twitter Edit on GitHub