Effects in NgRx are awesome. Not only do they utilize RxJS, but they abstract the complex logic from your Angular components to allow for state to be kept. Recently, in my fun side project to continue my learning of all of the above, I ran into a situation where I was creating an item which a parent item that was held in state needed to know about. Initially, I thought to dispatch an event to the store in my component, but I ran into a race condition in that the initial update didn’t complete in time so the parent state did not have the latest. Plus, this felt dirty.
Taking a step back, and after some trial and error, I figured I would lean on my effects and actions already built. After thinking it through, it made sense; the RxJS streams already tell me when data is ready as it streams down, plus action endpoints already trigger these effects to start the stream.
A great reference book that goes deeper into this topic: Reactive Programming with RxJS 5: Untangle Your Asynchronous JavaScript Code
An example in practice
Imagine this scenario. You have a container that holds a relationship of items. This container also keeps track and displays a summation of values that live on each item. When an item is created, updated or deleted, the container should know that the summation has changed based on the child items and it should display the new value.
Please note, the example code makes assumptions that the reader is familiar with the NgRx and RxJS patterns.
Actions are the hooks
For my actions, I already had a GET
setup, but it relied on filtering out the single item from the collection state to cut down on HTTP requests, which is typical. I needed to cause a true HTTP#Get
request to update the record and subsequently update itself in the collection state.
I ended up creating a new action to watch for around refreshing the record and passing through the correct payload.
1 | export enum EContainerActions { |
2 | ... |
3 | RefreshContainer = 'REFRESH_CONTAINER', |
4 | RefreshContainerSuccess = 'REFRESH_CONTAINER_SUCCESS', |
5 | } |
6 | ... |
7 | export class RefreshContainer implements Action { |
8 | public readonly type = EContainerActions.RefreshContainer; |
9 | constructor(payload) { } |
10 | } |
11 | |
12 | export class RefreshContainerSuccess implements Action { |
13 | public readonly type = EContainerActions.RefreshContainerSuccess; |
14 | constructor(payload) { } |
15 | } |
Handling the update in the reducers
In my reducer file for the container, I can now handle the case when the refresh success call is generated. This is done by mapping through the collection and replacing the single record based on the match of the id.
1 | export const conatinerReducers = ( |
2 | state = initialContainersState, |
3 | action, |
4 | ) => { |
5 | switch (action.type) { |
6 | ... |
7 | case EContainerActions.RefreshContainerSuccess: { |
8 | return { |
9 | ...state, |
10 | containers: state.containers |
11 | .map( |
12 | container => ( |
13 | container.id === action.payload.id |
14 | ? action.payload : container |
15 | ) |
16 | ) |
17 | }; |
18 | } |
19 | ... |
20 | } |
21 | }; |
The effects
Typically, it’s not normal to have an effect for success actions, but in my case I needed to hook into them. To update the scenario for the blocks of code below, an item has been created and that effect has been kicked off with an ending observable for the CreateItemSuccess
.
1 | @Effect({dispatch: false}) |
2 | createItemSuccess$ = this.actions$.pipe( |
3 | ofType(EItemsActions.CreateItemSuccess), |
4 | map(action => action.payload), |
5 | tap((item) => this.store.dispatch( |
6 | new RefreshContainer(item.container.id) |
7 | ) |
8 | ) |
9 | ); |
A few things are happening in the above block of code:
- This effect is watching for the action for
CreateItemSuccess
. When it catches the event, it starts the stream with the payload - Since I am wanting to just dispatch an event to the store, I use the
tap
method to drop into the stream - This effect does not need an observable to finish it out as nothing is observing it. In order to signify this, I include the
dispatch: false
in the@Effect
decorator
1 | @Effect() |
2 | refreshContainer$ = this.actions$.pipe( |
3 | ofType(EContainerActions.RefreshContainer), |
4 | map(action => action.payload), |
5 | switchMap((containerId) => |
6 | this.containerService.get(containerId)), |
7 | switchMap( |
8 | (container) => of(new RefreshContainerSuccess(container)) |
9 | ) |
10 | ); |
Happening:
- Since the
createItemSuccess$
effect ultimately called theRefreshContainer
action, this effect picks that up and starts the stream - With the payload sending in an
id
, we can call off to a service to fetch the latest record from the database - This record is then
switchMap
‘d into a new observable to call off theRefreshContainerSuccess
action
1 | @Effect({dispatch: false}) |
2 | refreshContainerSuccess$ = this.actions$.pipe( |
3 | ofType(EContainerActions.RefreshContainerSuccess), |
4 | map(action => action.payload), |
5 | tap(container => |
6 | this.store.dispatch(new GetContainer(container.id)) |
7 | ), |
8 | ); |
Finally, the result:
- When the
RefreshContainerSuccess
action is called fromrefreshContainer$
, this effect picks up the stream and the payload - Again, I tap into the stream to dispatch the final event to the store to update to the latest container
- Using the
dispatch: false
key value pair again tells the stream that nothing is listening to it so end the stream
Prevent crashing
One of the key items to take away from the above code blocks is the use of @Effect({dispatch: false})
. In each of the effects that utilize this, they end up not needing to complete the stream. This means that nothing is subscribing to them so without that key value pair, the stream would live on until something picks it up. With this key value pair, I am effectively telling the stream that as soon as the last function executes, the life of it is done. This keeps the garbage collection up to speed so the streams won’t crash the browser.
Keeping up to date
With all the logic residing in my effects, the component for observing on the store state is super simple:
1 | // Some component |
2 | @Input() containerId; |
3 | |
4 | public container$: Observable<IContainer> = this.store.select(selectContainer); |
5 | ... |
6 | |
7 | ngOnInit() { |
8 | this.store.dispatch(new GetContainer(this.containerId)); |
9 | } |
Because the container$
is observing the store, any action that is dispatched to the store that effects that initial selector, will keep it up to date. This allows me to trigger an update from anywhere in the application and know that the container state will be true.
With the use of the streams, I have removed the race condition and instead proactively made sure that the refresh of the container does not happen until the success actions are called from getting latest data.
Wrapping up
I love RxJS and how NgRx allows me to not have to worry about the complicated state in my components. Moving all that logic to the effects helps encapsulate the complexity and keeps the components super simple. I highly suggest to use the actions and effects combo whenever possible.