Using @ngrx/component-store — Exploration
This is part 2 of a series. Part 1 is an introduction to @ngrx/component-store.
In the previous part of this series, we went over how to set up a component store. We also discussed when you should consider using a component store and some of the benefits of using a component store. Now let’s dig in and have some fun with the component store we set up in the previous part. All the code from this part can be found here on GitHub.
Keeping Components In Sync
One of the first and most common use cases when using @ngrx/component-store is when keeping complicated interactions between components in sync. In this example, we will display two editors when a user clicks an edit button in the table. To add one more component to the mix, we will add a component that will display the name of the person being edited. All components will stay in sync regardless of which component is edited.
In this example, notice that the person in the table is not changed. The display component and two edit components are staying in sync. Let’s look at the code to make this happen.
The first modifications we make are to the PersonState
. By adding the editorId
, we can create selectors that can grab the current person if needed. It also would allow us to compare the editorId
to the id
of the person in a row to potentially change buttons or CSS styling based on the item being edited. Next, we add the editedPerson
. This is important because we want the store to be immutable. We could just add a selector that would grab the Person
being edited out of the store and then change it to keep everything in sync. Unfortunately, this would mutate our state. Side-effects like that cause our UI to be unpredictable and negate a lot of the benefit of using @ngrx/component-store.
The next modifications are some simple selectors for the editorId
and the editedPerson
. Notice the debug code using tap()
and console.log()
. Because selectors are just pure RxJS observables, we use pipe()
and any RxJs operator we want. This is often useful for debugging and tracking down interactions in our component store. On an actual project, I suggest that you look into creating a resuable logging RxJS operator. There are many examples out there and you can tailor your operator for your needs.
Next, we add some simple updaters to allow us to change the new properties on the state. Remember in your updaters to avoid mutating the state. We do this in this example using the object spread operator. With strict null checks enabled, TypeScript will prevent null
from being passed to our updaters. This is why we specify number | undefined
for example.
The final change is to add an effect to our PersonStore
. Effects in @ngrx/component-store are very similar to NgRx Effects. The major difference is that in NgRx, you react to Actions. In @ngrx/component-store, there are no actions. Calling the editPerson()
method on the PersonStore
with the id
to edit will cause our effect to emit a value that will be handled by the pipe()
on the personId$
observable created by the call to this.effect()
. This is a little less flexible than NgRx Actions and Effects, but still allows us to present a consistent API to users. A consistent API allows a consumer of our PersonStore
to know to call editPerson()
with an id
. Our effect can handle calling setEditorId(id)
and getting the personToEdit
and calling setEditedPerson(personToEdit)
.
There are some interesting things to note about this effect. On line 4, we call withLatestFrom(this.people$)
. Because the parameter to an effect is an RxJS observable, we can use RxJS operators to get data from other selectors to use in our effects. Remember, that withLatestFrom()
will wait for an observable to emit before emitting itself. If your observable hasn’t emitted, you may need to call startWith()
on the observable passed to withLatestFrom()
.
On line 13, we use the spread operator to create a clone of the personToEdit
. This will help us to avoid accidentally mutating the actual Person
from the people
array.
Effects can be tricky to get right. Having a sound understand of RxJS observables and the pipe()
operator will help in debugging. A generous helping of tap()
or a custom logging operator will be very valuable to trace what is emitted when.
Now that PersonStore
is ready, we need to wire things up to the UI. To get into edit mode, we added a button to the PersonListComponent
that has the following click handler (click)=”editPerson(element.id)”
. We then add the editPerson()
method to the PersonListComponent
.
This method is really simple. We just call PersonStore.editPerson()
with the id
passed into the handler. This is the power of using effects. We provide a very simple interface for consumers to be able to do powerful things with our store.
To get this data into the PersonContainerComponent
, we add an observable for the editedPerson$
selector.
editedPerson$ = this._personStore.editedPerson$;
We then need to get the template to show our new components that we want to keep in sync.
We use the async
operator to grab the last emitted value from the editedPerson$
observable. The components then are just simple Angular components. However, there is a little change that needs to be made to the <component-store-edit-person>
component to update the store so that all of our components update when the Person
is edited.
On the <input>
s above, you will notice that we have added an ngModelChange
event that calls personEdited()
. This allows us to respond when Angular detects that the <input>
has changed. In edit-person.component.ts
we simply call PersonStore.setEditedPerson()
with the current value of the person
property. Because of the unidirectional flow of @ngrx/component-store, the data updates in all components. You don’t have to worry about updates cycles or other weirdness caused by editing in two places at once.
Saving or Cancelling Updates
At this point, we have a fun toy, but it doesn’t really have any useful functionality. None of our updates can be saved or cancelled. Also, once you edit a component, you can’t stop editing. The editor is always open. Let’s fix that. We will create a SavePersonComponent
and add it to the DisplayPersonComponent
and the EditPersonComponent
. This will allow us to save or cancel edits to the current Person
from any of the three components.
To accomplish this, the SavePersonComponent
is a simple Angular component that has two buttons. They each have a click handler. The important part is the .ts
file.
In our SavePersonComponent
, we inject the PersonStore
and then call cancelEditPerson()
or saveEditPerson()
for the appropriate action. By keeping the API for our PersonStore
simple and consistent, we can make it easier for people to interact with the PersonStore
. You don’t have to inject the PersonStore
here. You can keep these components as pure presentation components and only interact with the PersonStore
in your container component. We are injecting them into the child components for demonstration purposes. When using the @ngrx/component-store in production, your team should decide on a pattern and stick to it. Consistency is key when working with any NgRx technology in a team.
Now that we understand how simple it is add the save and cancel functionality to our components, lets look at what changes need to be made to the PersonStore
. First we will investigate the cancelEditPerson()
because it is the simpler change.
There are many different approaches we could take to cancel edits. We could create a new effect or we could go with an even simpler route. I chose to demonstrate that the PersonStore
is just an injectable Angular service. This means that we can just add simple methods to it and call the updater methods. We can extract the updater calls to clearEditedPerson()
because this same logic will be used on a successful save.
The cancel method demonstrates why editedPerson
is important. By having a clone of the Person
being edited in the PersonStore
, we can update this Person
to keep our editors in sync, but easily cancel changes and not effect the actual table.
The save method is a little more complex. Here are the code changes for the save functionality:
As you can see, there were quite a few changes for the save functionality. Again, we could have used an effect here, but I wanted to show another pattern that is often useful. There are times when an effect isn’t the best solution. In those cases, we can add an observable to the store object. You can see this in the PersonStore
on line 4. This pattern can be used to give “psuedo-actions”. One good example of where I often use this is to add a save notifier to acomponent store. I really don’t want to add a property to the state that I have to set and then clear to notify components of a successful save because it can be error-prone and fragile.
The saveEditPerson()
method is super easy. It just emits on the saveEditPerson$
observable. The real “magic” is in the constructor. On line 12 we construct an observable pipeline that brings in other important pieces of data and then calls switchMap()
to the savePerson()
method from the StarWarsAPIService
. In our example, this just emits the value passed in after a delay()
. On line 17, we subscribe()
to the observable created. This is important because if we don’t use an effect, nothing will happen with our observable unless there is a subscriber.
On line 19, we have our success handler for the subscription. When a save is successful, we call updatePerson()
which is a new effect to handle updating the array. Then we call clearEditedPerson()
to go back to a state where we aren’t editing.
On line 33, we have a new effect called updatePerson
. This effect combines the people$
selector with an observable of updated Person
objects. It then locates the current person in the people
array and replaces it with the new edited person if it is found. Notice on line 43 that we are using a spread operator to clone the array before making changes. This is to help prevent unwanted mutations of the store. In a real production app where you would have more than just 9 items, I recommend using a more robust method for updating arrays.
Conclusion
At this point, our demonstration is fairly useful. You can edit items from the table and either save or cancel those changes. However, if a user starts to edit one item and then clicks the edit button for another row, their edits are discarded and the editor happily opens the new row. The user can also save items that have not been edited at all. These are not good user experiences and are something that would be easy to solve using the tools we discussed in this article. If you are learning @ngrx/component-store, I recommend attempting to create a solution on your own. You can find the code from this article on GitHub.
I hope that you have an idea of the usefulness of the @ngrx/component-store and a few ideas for how you can use a component store in your applications. Feel free to message me with any questions or comments. Most of all, have fun exploring this great new way to provide consistent state to UIs.
Part 3 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/