Taking Advantage of NgRx Effects

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export enum EContainerActions {
...
RefreshContainer = 'REFRESH_CONTAINER',
RefreshContainerSuccess = 'REFRESH_CONTAINER_SUCCESS',
}
...
export class RefreshContainer implements Action {
public readonly type = EContainerActions.RefreshContainer;
constructor(payload) { }
}

export class RefreshContainerSuccess implements Action {
public readonly type = EContainerActions.RefreshContainerSuccess;
constructor(payload) { }
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const conatinerReducers = (
state = initialContainersState,
action,
) => {
switch (action.type) {
...
case EContainerActions.RefreshContainerSuccess: {
return {
...state,
containers: state.containers
.map(
container => (
container.id === action.payload.id
? action.payload : container
)
)
};
}
...
}
};

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
2
3
4
5
6
7
8
9
@Effect({dispatch: false})
createItemSuccess$ = this.actions$.pipe(
ofType(EItemsActions.CreateItemSuccess),
map(action => action.payload),
tap((item) => this.store.dispatch(
new RefreshContainer(item.container.id)
)
)
);

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
2
3
4
5
6
7
8
9
10
@Effect()
refreshContainer$ = this.actions$.pipe(
ofType(EContainerActions.RefreshContainer),
map(action => action.payload),
switchMap((containerId) =>
this.containerService.get(containerId)),
switchMap(
(container) => of(new RefreshContainerSuccess(container))
)
);

Happening:

  • Since the createItemSuccess$ effect ultimately called the RefreshContainer 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 the RefreshContainerSuccess action
1
2
3
4
5
6
7
8
@Effect({dispatch: false})
refreshContainerSuccess$ = this.actions$.pipe(
ofType(EContainerActions.RefreshContainerSuccess),
map(action => action.payload),
tap(container =>
this.store.dispatch(new GetContainer(container.id))
),
);

Finally, the result:

  • When the RefreshContainerSuccess action is called from refreshContainer$, 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
2
3
4
5
6
7
8
9
// Some component
@Input() containerId;

public container$: Observable<IContainer> = this.store.select(selectContainer);
...

ngOnInit() {
this.store.dispatch(new GetContainer(this.containerId));
}

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.

Filed under: Code