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_OPEN
andSNACKBAR_CLOSE
. - Next, we define the
SnackbarOpen
class that implements theAction
interface. The payload object expects amessage
, along with an optionalaction
type and theconfig
for the snackbar. We’ll dispatch this action to our store when we want to open a snackbar notification. - Next, we defined the
SnackbarClose
class. We’ll dispatch this action to our store when we want to close the snackbar notification. - Finally, we export the
SnackbarAction
type 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
SnackbarEffects
class 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 theActions
andMatSnackBar
instances. - We then define a new
closeSnackbar
property that has the@Effect()
decorator. Note that we use thedispatch
option to instruct ngrx that this effect will not dispatch another action. When theSnackbarClose
action is dispatched we simply invoke thedismiss()
method on theMatSnackBar
service to close the snackbar that is currently being displayed. - Next, we define the
showSnackbar
property where we are observing for theSnackbarOpen
action 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 theSnackbarClose
action.
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()
theUpdatePower
action to the store. - An
updatePower
effect uses thePowersService
to update the entity using theHttpClient
, which returns anObservable
object. - When the observable’s next notification is received the effect dispatches the
UpdatePowerSuccess
action. - The
updatePowerSuccess
effect then dispatches theSnackbarOpen
action, providing themessage
andaction
strings.
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.