Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

NgRx + Material Snackbar

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:

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:

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:

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:

  1. We dispatch() the UpdatePower action to the store.
  2. An updatePower effect uses the PowersService to update the entity using the HttpClient, which returns an Observable object.
  3. When the observable’s next notification is received the effect dispatches the UpdatePowerSuccess action.
  4. The updatePowerSuccess effect then dispatches the SnackbarOpen action, providing the message and action 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.