Brian Love
Google Developer Expert in Angular, software engineer and skier located in Denver, CO

NgRx + Material Snackbar

Reading time ~6 minutes

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 and SNACKBAR_CLOSE.
  • Next, we define the SnackbarOpen class that implements the Action interface. The payload object expects a message, along with an optional action type and the config 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 the constructor() function for any objects that need to be injected. In this example, we need both the Actions and MatSnackBar instances.
  • We then define a new closeSnackbar property that has the @Effect() decorator. Note that we use the dispatch option to instruct ngrx that this effect will not dispatch another action. When the SnackbarClose action is dispatched we simply invoke the dismiss() method on the MatSnackBar service to close the snackbar that is currently being displayed.
  • Next, we define the showSnackbar property where we are observing for the SnackbarOpen action to be dispatched to the store, which will invoke the open() method, providing the values that are defined in the payload. After 2 seconds we dispatch the SnackbarClose 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:

  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.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).