Using @ngrx/component-store — Introduction
The @ngrx/component-store is a relatively new library from the team that brought us NgRx. I have some great experiences using this library and would like to share some of what I have learned. This series assumes that you have some familiarity with NgRx basics. If you don’t, I recommend a quick read from the NgRx website. This will be a multi-part series that will take the following form:
Part 1 — An introduction to the component store. This will be a discussion about my experience on when to use the component store and provide a quick introduction for how to set one up.
Part 2 — Will be a more in-depth look at the component store and some of its uses that I have encountered.
Part 3 — A small detour into Spectator a great library for testing Angular applications. We will use this library to do our unit testing of the component store that we have discussed this far.
Part 4 — Using Spectator to test an @ngrx/component-store application. This will discuss how to set up tests that rely on our component store using Spectator. It will also discuss how we can use Specatator to test our component store itself.
What Is the Component Store?
The @ngrx/component-store is a library that bridges the gap between Angular services and full-blown NgRx. I look at the component store as a sub-set of NgRx that gives many of its benefits. You should consider using @ngrx/component-store when you have an isolated tree of components that would benefit from a unidirectional dataflow.
NgRx and @ngrx/component-store compliment each other and can co-exist painlessly in an application. In my current job, I work on the front-end for a cyber-security product. I have NgRx and @ngrx/component-store interacting with each other and am very happy with the results.
There are some things that you cannot do with the component store that you can do with NgRx. One big thing you do not get with the component store are Actions. This is a big deal because you cannot trigger multiple Effects or Reducers at the same time without Actions. You cannot use the the redux dev tools including the ability to inspect the store and the time-travelling debugger. Finally, the component store is not as scalable as NgRx. NgRx is designed to scale as your application grows. It helps to solve some complicated problems that arise when you have a large inter-connected application.
So far we have discussed what the component store is not. What benefits do we get from using the component store? The component store is a natural transition from Angular services to something more interconnected. It is a stepping-stone on your path to NgRx. It encourages good patterns that will make moving to NgRx much easier.
The component store is scoped to the component tree where it is injected. This means you get many of the benefits of NgRx, but all of it is cleaned up when your component goes out of scope. One example where it has worked very well for me is in adding a dashboard system to an application. The application includes a generic system that can load user-specific dashboards from storage. Users can edit and save said dashboards. During both edit and display, real data can be used. All of the data and user designs are scoped to the specific dashboard the user is viewing at the time. If multiple dashboards are used in an application, the developer doesn’t have to worry about how to partition data in memory and how to clean it up when the user moves on to another part of the application.
When Should I Consider @ngrx/component-store?
There are a couple of reasons that you should consider using the component store.
First, if you are injecting a service over and over inside of a component tree. This is a good time to consider if you have a flow that would benefit from the component store and a unidirectional data flow.
Second, Angular services often go from Subject<T>
to BehaviorSubect<T>
as the component interactions become more intelligent or complex. When this happens ask yourself if you are trying to create a component store lite. By component store lite, I am referring to a tendency to reproduce features of NgRx or the component store in Angular services. Using BehaviorSubject<T>
is often the first step down this path. If you need more advanced Angular services, consider @ngrx/component-store instead. Someone else has written and tested the code for you.
Finally, for components that are interconnected do you need to guarantee one source of truth or a guaranteed path for updates to flow? For example, many of us have experienced a situation where two components can edit or modify data, but you only need certain modifications to happen if the data is new. In this case, we often write a lot of code fighting against the circular data path. NgRx and the component store have solved these issues by making dataflow unidirectional. This makes them a natural fit. This situation often arises in very reactive parent/child component designs. When updates start to become difficult to reason about or you are chasing down update loops or bugs, consider using the component store.
When Should I Consider NgRx Over the Component Store?
There is almost always a natural progression from Angular service to component store to NgRx. When you need it, you know that you need it — is a phrase quite often expressed in the context of NgRx. There are times that make NgRx superior to the component store.
If the data is more global and not scoped to a component tree, then NgRx is a better fit. For example, in some applications a cache of user permissions to control UI elements is useful. Permission caches are global and a natural fit for NgRx.
If an @ngrx/action is necessary for a workflow, you have to use NgRx because actions do not exist in the component store. Any time you want to fire multiple effects or reducers, you should consider using NgRx over the component store.
If I want to use entities, NgRx has many tools that make it a better fit than the component store. A need for entities is often a sign that the scope and scale of your data and application is more than the component store can handle.
Finally, as your application grows, you will want to use a component store in more places than just the component tree that it is injected in. When this happens, it is time to consider using NgRx over the component store. This is part of the natural growth of an application. When planned properly, moving from Angular services to component store to NgRx should happen naturally and be relatively painless.
Using @ngrx/component-store
To demonstrate how to use the component store, I have set up a project on github that uses the Star Wars API to get data for the application. Since this API is read-only, we will only simulate write commands to show how they can be done. In this introduction we will explore how to set up a component store in a component tree and how to populate it with data. In the next article, we will explore how to do more interesting things with that data.
To add @ngrx/component-store to your application, you can follow the instructions on the installation page. My preference is to use the angular cli and ng add
.
ng add @ngrx/component-store@latest
Now that we’ve added @ngrx/component-store to our application, we need to add the store to a component tree. You should read the usage page for some examples of how this can work. I have found that creating a service that you inject as a store works the best. This gives flexibility to change to NgRx in the future. The component store service becomes a façade for the NgRx backing store.
The example that I’m going to show uses the following parent/child design.
We will have a parent component PersonContainerComponent
. This will be our smart component in the tree. We will inject our component store into the component tree at this level. This component will also house our intelligence for this component tree. The PersonEditorComponent
is a presentation component that houses a simple form that will be used for simple CRUD interactions with the component store. The PersonListComponent
is another presentation component that displays a table that shows all the people in the store.
Let’s start with the code to set up a component store.
There are a few things of note in this file. First, I often embed the state interface in the same file as the component store. This is a purposeful violation of my personal rule to split models from business logic. I do this to encourage thought about the state object. If it gets large or complex, it is often a sign to move to NgRx.
Second, you will note that there is no providedIn:
in the @Injectable()
decorator. This is so that we have to inject this store in the component tree where we want it. If this store was injected in the root injector, it would be global and not isolated to the component tree where it is needed. If you need a globally injected store, it is time to use NgRx.
Third, we provide a default state using super()
in the constructor. Creating a defaultState
and providing it in the constructor is a habit that has saved me from frustrations on many occasions. I recommend this practice when working with a component store.
You can see on line 19 that we created a people$
selector. Selectors in @ngrx/component-store are very similar to selectors in NgRx. They take a projector that takes the state and returns an observable. We will use this selector to provide the people in the store to a table for display.
On line 21, there is an effect to allow us to load people into the PersonStore
. Effects in @ngrx/component-store are like a combination of the Effect
and the Reducer
from NgRx. This makes it more difficult for component store services to scale like NgRx. It takes more work to combine reducers in meaningful ways. If you find yourself wanting to combine reducers or make them more advanced, you may want to consider changing to NgRx.
Adding PersonStore to a Parent Component
Now that we have a working PersonStore
, how do we add it to our component tree and use it in our components? We want to add it to our smart component — PersonContainerComponent
. Also, how do we use data from our StarWarsApiService
?
Notice that we are using the component providers
array to inject PersonStore
. This scopes PersonStore
to this point in the component tree. This guarantees that this component and any children will get this instance of the store. It also allows the PersonStore
to be automatically cleaned up when the PersonContainerComponent
is destroyed.
Once it is provided to the component, we can inject the PersonStore
like normal and call the loadPeople()
effect to load the people that we get from the StarWarsApiService
(line 20).
Now that we have the PersonStore
injected into the PersonContainerComponent
, how do we provide it to child components? We just need to inject the PersonStore
in the PersonListComponent
. We will use the store to populate the table. We will do the same thing for the PersonEditorComponent
in the next part of this series.
Since this is a presentation component, this is a pretty simple file. We injected PersonStore
in the constructor. This is basic Angular at this point. Any child of the component where the store is provided will have access to that instance of the PersonStore
. A common pattern that I follow is to provide the people$
selector on the component as a property. Doing this allows us to bind to this observable in the HTML. I recommend providing observables and using the async
pipe if the component input does not take an observable. This will prevent issues like memory and subscription leaks that can happen when using Observable<T>.subscribe()
. This is the result of the example code so far.
You can find all of the code for this example in this github repo.
https://github.com/xocomil/ComponentStore
In the next part of this series, we will extend the example application to do some more interesting things using @ngrx/component-store.
Part 2 of this series can be found here.
Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:
- Expert panels and Q&A sessions with the speakers
- A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
- Hands-on workshops
- Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget
We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.
https://2022.ng-conf.org/