Should You Use NgRx, in the First Place?
This is a question that cannot be answered easily. Like every framework or library out there, it was made to do a certain thing, and to do it well. If your app is tiny and does not need an elaborate state management system, why bother.
You can achieve a clean architecture without the use of NgRx. However, this requires experience with Angular and software architecture as a whole.
If you are not familiar with reactive principles and get a headache thinking about observables and composing data streams, this might not be for you. When choosing NgRx, you are committing to the reactive architecture, and if you try to fit it with an application that does not make use of the principles, you are going to have a really bad time.
Let’s do a quick reality check.
Are you familiar with:
- the async pipe
- OnPush change detection
- store like “Subject in a service” implementations
- immutability
- composing data streams
- pure functions
- smart (host) and dumb (presentational) components
While the above is by no means an exhaustive checklist, the chances are that you might not be ready for this step on your Angular journey if you are not familiar with some of the concepts. However, I do strongly recommend to go read up on them. It will make your architecture stronger and boost performance in your Angular application.
If you are not familiar with most of the concepts above and still try to bring NgRx into a project that is not using the principles mentioned, you will be disappointed, and it will do more harm than good.
NgRx comes with its own concepts to learn. They are by no means hard to understand, but on top of the above, you will have to make yourself familiar with:
actions
reducers
effects
selectors
These concepts are very simple and easy to pick up, but at first, it might be overwhelming to figure out how to use them in your environment to get all the goodness everyone is talking about.
However, if you are used to these principles, you are working on a medium to large scale application and need a unified way to handle your state, you are in a good position to give NgRx a try. While switching might be quite an investment, it will feel very clean afterward.
NgRx is a powerful tool, and as such, has to be wielded like one. If you do not know what you are doing and are fighting its principles, you will end up with poor results.
In my opinion, one of the beautiful sides is that you are forced to discuss and plan your architecture even more. You can and should brainstorm the required actions to perform in you team and stake out which state you need to store where. While this leads to more communication, it will help cross-validating your architecture. But again, using this tool will not magically lead to better quality code. So always be on the lookout to question the practicability of your architecture. Unit tests are always a good indicator, whether or not your system is nicely decoupled.
Indicators for Abuse
I get it. Not everyone has the time to spend tons of hours refining their architecture. We all see the conference talks, where new tech is introduced, and good arguments are laid out for why it should be used. It might even fit your project perfectly, but once you start using it you are wondering why things aren’t going as smoothly as was promised. This again is the problem of wielding a powerful tool the wrong way. But how do we know when we are doing things wrong.
This will come with a lot of experience with software architecture. A certain esthetical feeling is necessary to understand when something is off. This feeling should turn into a set of arguments why a certain implementation is just not good. This does not necessarily need to relate to performance. But most of the time, it comes down to encapsulation and code re-use. Of course, your application can run with a completely coupled and messy architecture. However, if the new fancy tech you are employing feels terrible to use, then there is a chance you are not using it the right way, and the integration into your system might be a major pain point.
I have compiled a small list of things I have seen that doen not sit well with NgRx. This is by no means an exhaustive list, and there are always exceptions to the rule. But if you see yourself excessively using some of the practices I mention, there is a chance that you should question your architecture.
Reacting to the Success of an Action
this.store.dispatch(action()).subscribe()
If you hope for an api like the above, where you dispatch an action and lets you react to the action “to be done” or return “successfully/erroneously” you are in for a bad time, as actions are fire and forget.
Actions are dispatched, and that is that. Effects
and/or reducers can react to said action
, and in the case of effects
, dispatch another action. While you should definitely avoid a big chain of effects and actions, there is no telling when an action
is done. There is no definition for when an action
is done.
The idea is that you dispatch an action
, and eventually, the state will change. It might be instantaneously (e.g. optimistic updates) or after a side effect successfully retrieved data.
Therefore, the proper way to “react” to an action is… you do not. You do not need to. You have your selector waiting for your state to change and give you the new data you are working with.
Actions go into the system. State updates comes out of the system.
Reading Data Synchronously
There should not be the need to read your data synchronously. If you find yourself doing a lot of:
let data;
this.store.select(mySelector).subscribe((value) => {
data = value;
});
You might be doing something wrong.
If you need to show your data in the template, compose selectors or streams to result in emitting your view model and bind to the stream via async
pipe.
If you need the current user to make an http request, you can select
, take(1)
and switchMap
or use the withLatestFrom
or other combining operators.
Components Mutating Their Inputs
A big principle in your reactive design is immutability. Instead of mutating a list, or an object, you will create a new one with the updated properties. If your dumb component writes on the objects you feed it via inputs, the changes might be gone once the store updates, as well as might not be detected via Angular’s change detection.
const user = { id: 1, name: 'Old Name', age: 26 };
// DO NOT mutate an object:
user.name = 'New Name';
// DO create a new object instead:
const newUser = { ...user, name: 'New Name' }
If you have a component to select an object from a list and highlight an active item, do not rely on comparing their references. This can get messy quickly and make you bend over backward when working with the store.
const users: User[] = [
{ id: 1, name: 'Alice', age: 26 },
{ id: 2, name: 'Bob', age: 37 },
];
// DO NOT compare by reference
const selectedUser: User;
const foundUser = users.find(user => user === selectedUser);
// DO compare by ID:
const selectedUserId: number;
const foundUser = users.find(user => user.id === selectedUserId);
All Properties of all Components are in the Store
If you only use a single global app state and tie every single component to it, it will be a coupled nightmare.
You will still need smart and dumb (presentational) components. Your dumb components will be shareable and run in different contexts. Do not tie them directly to the store and have an input flag to configure which property they read. Yes. I have seen that. If you have a huge app state interface that contains all properties of several components, you are missing a big red flag.
In the later versions of NgRx, you can make use of scoping your store to feature modules. Well, it’s not really scoping, as the stores of your feature modules are also in the global store, but it makes maintaining your feature modules and selecting in your smart components a lot more managable.
Not Everything Goes into the Store
In your application, there will be many kinds of states for you to maintain. This could include things like:
- which route the application is on
- which user is logged in
- which settings are currently set (think: language, locale, …)
- a list of favorite items
- the contents of a shopping cart
Let’s focus on just the current view in your app. Things you would be showing there could be:
- a list of items and whether that item is favorited or in the cart
- collapsible boxes
- something that is zoomable and the currently set zoom level
- a loading spinner while data is loading
All of the above can be considered part of the state of your application.
If you look closely at the examples above, you might notice that there is a fundamental difference between the two lists. It is the state that is needed to be accessed and manipulated application-wide (i.e. globally), and there is the state that needs to be managed only for your current view (i.e. locally).
For the first set I mentioned, I would usually implement a service that exposes the state via an Observable
. A UserService
for example would provide me with a user$
stream, which allows my components and other services to react to user changes. Usually, this is backed by a BehaviorSubject
which is ideal for holding state information. This is also known as “Subject in a Service” pattern and could look like this:
@Injectable({
providedIn: 'root',
})
export class UserService {
private readonly _user$ = new BehaviorSubject(undefined);
public readonly user$ = this._user.asObservable();
public setUser(user?: User): void {
this._user$.next(user);
}
}
Notice how this service is provided in the root injector of our application so that every other service, component, directive, or even pipes can access it.
The second set of state I listed above, I handle and hold within the host component, or rather a service tightly coupled to the host component:
@Component({ ... })
export class ItemOverviewComponent implements OnInit {
public items$: Observable- = of([]);
constructor(private readonly itemService: ItemService) {}
public ngOnInit(): void {
this.items$ = this.itemService.getItems();
}
}
In the above example, we hold a list of only valid items for that particular view. Imagine that we even react to route param changes to update this view. No other part of the application will need to access this list of items, so it is fair to be considered as local state, which we do not need to maintain in the store.
When reviewing several projects making use of NgRx, I have found that everything was put into the store, thus creating a global application state with hundreds of properties.
While several people are successful in running this approach, it requires cleanup of the old, local state. This is to make sure that when re-entering the view, you are not presented with old or out-of-sync data.
I have also encountered the situation that pretty much every minor component was tied to the store, thus tightly coupling the whole application to it. This led to not being able to tell which property on the state went into which component and turned out to be a nightmare to maintain.
Therefore, think carefully about what you put into the store. Not everything goes into the store. Not every property that has a template binding goes into the store. You will still need presentational (or dumb) components that you can feed via inputs and re-use across the app.
If you want to implement your local component state in a store-esque fashion, NgRx got you covered…
@ngrx/component-store
The component store is a local, stand-alone, store-like implementation, similar to the “Subject in a Service” pattern, offering a standardized store-like API for you. You can easily plug it into every service that implements the pattern and use it in your project right now!
It does not rely on the typical action
and reducer
pattern and rather lets you update your state via so-called updaters
and, of course, has effects
for side effects as well. Analog to reducers
, the updaters
also update the store by making use of pure functions and work pretty much similarly, except that no actions are dispatched, and the updater can be executed like a method call instead:
export interface ItemOverviewState {
items: Item[];
loading: boolean;
isCollapsed: boolean;
error?: string;
}
@Injectable()
export class ItemOverviewStore extends ComponentStore {
// side effect: loading items
public readonly loadItems = this.effect((trigger$: Observable) =>
trigger$.pipe(
switchMap(() => {
this.patchState({ items: [], loading: true });
return this.itemService.getItems().pipe(
tap(items => this.patchState({ items, loading: false })),
catchError((err) => {
this.patchState({
items: [],
loading: false,
error: err.message,
});
return EMPTY;
}),
);
}),
),
);
// observable list of items
public readonly items$ = this.select(({ items }) => items);
// observable view model
public readonly vm$ = this.select(({ items, ...vm }) => vm);
// updating the collapsed state
public readonly toggleCollapse = this.updater((state) => ({
...state,
isCollapsed: !state.isCollapsed,
}))
constructor(private readonly itemService: ItemService) {
super({ items: [], loading: true, isCollapsed: false });
}
}
@Component({
...
providers: [ItemOverviewStore],
})
export class ItemOverviewComponent implements OnInit {
public readonly items$ = this.store.items$;
public readonly vm$ = this.store.vm$;
constructor(private readonly store: ItemOverviewStore) {}
public ngOnInit(): void {
this.store.loadItems();
}
public toggleCollapse(): void {
this.store.toggleCollapse();
}
}
Notice how the state of the component is held in the store service and how this service is provided locally in the component injector. Once the component is destroyed, so will be this local store!
In order to get data out of the store, you can use the same kind of selector system, just like in the global store implementation. The best thing is to combine your selectors with the selectors of the global store in case you depend on it!
public readonly items$ = this.select(
this.state$, // component store state stream
this.store.select(getFavoriteIds); // select on the global store
({ items }, favoriteIds) => {
// combine the local items with the global favoriteIds and return
return ({ ... });
}
);
This is the way you can connect your component store to the store if needed. Your component store should not use its effects
to react to actions
. While possible, your local component should not be aware of which actions flow to your store. You can easily combine your selectors and react to store state changes.
The component store provides a good store-like experience, with the benefit of being very simple. You can implement a service extending the component store class and provide it in your component. Once the component is destroyed, so will be your component store, and no cleanup is needed. It will be done for you!
This is a good “Subject in a Service” alternative and might even help you wrap your head around reactive component architecture.
Selectors are Powerful
Selectors can do more than just reading a single property from your store. I am not going into technical detail and explain what makes selectors great. So here is why in a nutshell: pure functions allow memoization, and re-computation only occurs when inputs change, similar to pure pipes in Angular.
More importantly, selectors can be combined, making them a powerful tool to shape the data you want to see come out of your store. This is similar to how you would design an RxJS pipe to build your view model.
You can even have a selector transform your data, and there is no need to map it in your stream. Technically, it does not make much of a difference. However, if you use said selector multiple times, it saves you using your map operator multiple times across your application.
// Combine and transform in your selectors
export const selectProductWithCategories = createSelector(
selectProduct,
selectCategories,
(product, allCategories) => {
const categories = determineCategories(allCategories, product.categoryIds);
return { ...product, categories };
},
);
Conclusion
NgRx is an advanced tool. While I am not saying that you cannot start using it right away and being on a good path to good Angular architecture, you can shoot yourself in the foot like with every other tool. The tool is only as good as the user. If you try to force its use in an environment that it wasn’t made for, you will suffer, and it will slow you down.
Be mindful that when picking NgRx, you make a commitment to the architecture of your application. Angular has many ways for communication already, and you can build big enterprise applications without NgRx. You can easily implement store-like services, which will spare you the separation into actions
, reducers
, and effects
. However, if you like the single responsibility principle, you might quite enjoy doing so.
If someone on your team is really familiar with NgRx and can build the reducers
and effects
for you, you only need to work with actions
and selectors
and your component stays free from logic. However, the same can be achieved with service implementations. The downside of services might be that you will have to inject multiple services at some point and be familiar with which service does what and how they interact. In the case of the store, you will inject a single store, and the API will be the same. You need to know the actions
and selectors
available. Some might say that importing the services is more explicit, but you can easily lose track injecting many services.
NgRx is a powerful tool that does what it does well. You need to familiarize yourself with what that is and decide if it is a fit for your project, and I hope this article helps you (re-)evaluate your current position.
Personally, I am successfully using NgRx in a couple of projects, and it does fit my style of developing Angular architecture quite well!