Show and hide Material Snackbar using NgRx and RxJS with Angular.
Why?
Subscribing to the next and error notifications of an observable for all asynchronous events where we want to display a notification is repetative and cumbersome.
Here’s an example of a showing a notification using the MatSnackBar service in an Angular component:
export EditComponent {
powerChange(power: Power) {
this.powersService.updatePower(power).subscribe(
() => {
this.snackBar.open("Power Updated", "Success", {
duration: 2000
});
},
error => {
this.snackBar.open("Update Failed", "#fail", {
duration: 2000
});
}
);
}
}
In the example above we are displaying the snackbar notification to our users when the update to the Power entity is either successful or an error.
While this works with pure Angular, we can take advantage of the observer design pattern that is implemented by NgRx to dispatch an action to show a snackbar when an asynchronous event in an effect is successful or not.
Some of the advantages of using a redux-style approach to showing notification in our application include:
- We don’t need to repeat the code for showing and hiding notifications for each observable stream.
- We can delegate dispatching of the notification actions to our effects.
- We can dispatch a single action to update an entity in our component, greatly simplifing our component’s code.
- We take advantage of reactive programming with RxJS.
After implementing NgRx in our application the powerChange() method becomes:
export EditComponent {
powerChange(power: Power) {
this.store.dispatch(new UpdatePower(power));
}
}
That’s all folks! If you’re new to NgRx, I would recommend that you first read my NgRx: The Basics post.
Install
A working application that implements the example code in this post can be found at:
After cloning the repository, be sure to checkout the ngrx-refactor-2 branch:
$ git checkout ngrx-refactor-2
To install and start the server:
$ cd server
$ yarn install
$ yarn serve
Then install and serve the Angular client application:
$ cd client
$ yarn install
$ ng serve
Actions
To get started, we need to create actions for showing and hiding the snackbar notifications:
import { MatSnackBarConfig } from "@angular/material";
import { Action } from "@ngrx/store";
import { createActionType } from "../utils";
export const SNACKBAR_OPEN = createActionType('SNACKBAR_OPEN');
export const SNACKBAR_CLOSE = createActionType('SNACKBAR_CLOSE');
export class SnackbarOpen implements Action {
readonly type = SNACKBAR_OPEN;
constructor(public payload: {
message: string,
action?: string,
config?: MatSnackBarConfig
}) { }
}
export class SnackbarClose implements Action {
readonly type = SNACKBAR_CLOSE;
}
export type SnackbarAction = SnackbarOpen | SnackbarClose;
This code is located in client/src/app/state/shared/actions/snackbar.ts. Let’s quickly review:
- First, we define our action type constants:
SNACKBAR_OPENandSNACKBAR_CLOSE. - Next, we define the
SnackbarOpenclass that implements theActioninterface. The payload object expects amessage, along with an optionalactiontype and theconfigfor the snackbar. We’ll dispatch this action to our store when we want to open a snackbar notification. - Next, we defined the
SnackbarCloseclass. We’ll dispatch this action to our store when we want to close the snackbar notification. - Finally, we export the
SnackbarActiontype that represents a union of both of our actions.
Effects
With our actions defined, we now will wire up some side effects that will use the MatSnackBar service for opening and closing the notification:
import { Injectable } from "@angular/core";
import { MatSnackBar } from "@angular/material";
import { Actions, Effect } from "@ngrx/effects";
import { Observable } from "rxjs/Observable";
import { delay, map, tap } from "rxjs/operators";
import { SNACKBAR_CLOSE, SNACKBAR_OPEN, SnackbarClose, SnackbarOpen } from "../actions/snackbar";
@Injectable()
export class SnackbarEffects {
@Effect({
dispatch: false
})
closeSnackbar: Observable<any> = this.actions.ofType(SNACKBAR_CLOSE)
.pipe(
tap(() => this.matSnackBar.dismiss())
);
@Effect()
showSnackbar: Observable<any> = this.actions.ofType<SnackbarOpen>(SNACKBAR_OPEN)
.pipe(
map((action: SnackbarOpen) => action.payload),
tap(payload => this.matSnackBar.open(payload.message, payload.action, payload.config)),
delay(2000),
map(() => new SnackbarClose())
);
constructor(private actions: Actions,
private matSnackBar: MatSnackBar) {
}
}
Let’s review the code above:
- First, we define a new
SnackbarEffectsclass that has the@Injectable()decorator so that Angular’s dependency injection inspects theconstructor()function for any objects that need to be injected. In this example, we need both theActionsandMatSnackBarinstances. - We then define a new
closeSnackbarproperty that has the@Effect()decorator. Note that we use thedispatchoption to instruct ngrx that this effect will not dispatch another action. When theSnackbarCloseaction is dispatched we simply invoke thedismiss()method on theMatSnackBarservice to close the snackbar that is currently being displayed. - Next, we define the
showSnackbarproperty where we are observing for theSnackbarOpenaction to be dispatched to the store, which will invoke theopen()method, providing the values that are defined in the payload. After 2 seconds we dispatch theSnackbarCloseaction.
Don’t forget to update the EffectsModule.forRoot() or EffectsModule.forFeature() array argument to include the newly created SnackbarEffects class:
@NgModule({
imports: [
// code omitted
EffectsModule.forRoot([SnackbarEffects]),
],
declarations: [],
})
export class StateModule {}
Reducer
Now let’s define a reducer() function that will mutate the state of our application for each action we defined:
import { SNACKBAR_CLOSE, SNACKBAR_OPEN, SnackbarAction } from "../actions/snackbar";
export interface State {
show: boolean;
}
const initialState: State = {
show: false
};
export function reducer(state: State = initialState, action: SnackbarAction) {
switch(action.type) {
case SNACKBAR_CLOSE:
return { ...state, show: false };
case SNACKBAR_OPEN:
return { ...state, show: true };
default:
return state;
}
}
First, we define a new State interface with a single show property that is a boolean value that defaults to false.
We’ll toggle this value when the snackbar is opened or closed.
In the reducer() function we simply return the state of our application with the show value set to true or false based on whether the notification is being shown or not.
Implementation
With our actions, effects and reducers completed, we can now implement the dispatching of notifications in our other effects.
This assumes that you have already implemented actions and effects for handling things like updating or deleting entities.
In this example, we’ll wire up our SnackbarOpen action to the effect for the UpdatePowerSuccess action:
@Injectable()
export class PowersEffects {
@Effect()
updatePower: Observable<Action> = this.actions.ofType<UpdatePower>(UPDATE_POWER)
.pipe(
map(action => action.payload),
switchMap(power => this.powersService.updatePower(power).pipe(retry(3))),
map(power => new UpdatePowerSuccess(power)),
catchError((e: HttpErrorResponse) => Observable.of(new HttpError(e)))
);
@Effect()
updatePowerSuccess: Observable<Action> = this.actions.ofType<UpdatePowerSuccess>(UPDATE_POWER_SUCCESS)
.pipe(
map(() => new SnackbarOpen({
message: 'Power Updated',
action: 'Success'
}))
);
}
The flow of our actions in this example is:
- We
dispatch()theUpdatePoweraction to the store. - An
updatePowereffect uses thePowersServiceto update the entity using theHttpClient, which returns anObservableobject. - When the observable’s next notification is received the effect dispatches the
UpdatePowerSuccessaction. - The
updatePowerSuccesseffect then dispatches theSnackbarOpenaction, providing themessageandactionstrings.
Conclusion
We can react to asynchronous activities and notify the user using Material’s MatSnackbar service using a Redux approach to state management in our Angular applications using NgRx and RxJS.
In this example, I have a specific “success” action for updating the power entity.
While the needs of your application may differ, you may also consider a single HttpSuccess action that displays a generic notification message to the user.
This would slim up your effects and not require an effect for each action where you need to notify the user, having a single “global” effect that will dispatch the SnackbarOpen notification.