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

Brian Love

Angular + NgRx: Refactor Module

Refactor an existing application’s module to implement the Redux pattern using NgRx.

Series

This post is part of a series on using NgRX, the Redux-inspired, predictable state container for JavaScript:

  1. NgRX: The Basics
  2. NgRX: Getting Started
  3. NgRX: Refactor Module (this post)

Example Project

In this post I’ll be working with an example application that I wrote on New Year’s eve 2017. I spent about 6 hours developing a Tour of Heroes application that uses:

I’m also using Angular’s Material UI and Flex Layout modules. You can download the source code and follow along or fork the repository on GitHub:

Note that if you fork the repository, I’ll be working from the ngrx-refactor-1 branch.

The example project uses json-server to provide a simple REST API that our client Angular application can consume.

To install and start the server:

$ cd server
$ yarn install
$ yarn serve

The server is now available at: http://localhost:3000.

We have two resources available to us: heroes and powers

We also have static files being served out of the server/static directory. The example application uses a few images that are stored in server/static/img.

To install and start the client:

$ cd client
$ yarn install
$ ng serve

Now, navigate to http://localhost:4200 and you should see:

Tour of Heroes Application

Previously

Previously, we created a root StateModule and imported it into the AppModule. You can read through the post on getting started with NgRx.

This post will continue with the next steps for refactoring the PowersModule to use the Redux pattern that is implemented by NgRx.

Super Powers

Our application is modeled off of the Angular Tour of Heroes tutorial. We have two lazy-loaded modules:

  1. HeroesModule
  2. PowersModule

The heroes module manages the super heroes in our application, and the powers module manages the powers that can be assigned to a super hero - all super heroes need to have super powers! Well, perhaps not Iron Man, as Tony Stark doesn’t have any super powers. He’s just a “genius, billionaire, playboy, philanthropist”.

Our current application has NgRx installed and configured, but we haven’t implemented the Redux pattern into our application. The first step is to create actions and reducers for our Power model. Then, we’ll wire up effects that will use the PowerService. Finally, we’ll refactor our existing application to use the Store, dispatching actions and using selector functions to retrieve data from the store.

If you’re completely new to the Redux pattern, I suggest you read through my post on the basics of Redux and NgRx with Angular.

Actions

Let’s dive into implementing actions for our application. To start with, let’s create actions to load the array of powers from the REST API.

Create a new file src/app/state/powers/actions/powers.ts:

import { Action } from "@ngrx/store";
import { Power } from "../../../core/models/power.model";
import { createActionType } from "../../shared/utils";

export const LOAD_POWERS = createActionType('LOAD_POWERS');
export const LOAD_POWERS_SUCCESS = createActionType('LOAD_POWERS_SUCCESS');

export class LoadPowers implements Action {
  readonly type = LOAD_POWERS;
}

export class LoadPowersSuccess implements Action {
  readonly type = LOAD_POWERS_SUCCESS;

  constructor(public payload: Power[]) {}
}

export type PowersAction =
  LoadPowers
  | LoadPowersSuccess;

Let’s quickly review our initial actions:

While this is adequate for our application’s need to load the powers from the REST API, we also need to implement actions for:

I’ve gone ahead and created all of those actions for us:

import { Action } from "@ngrx/store";
import { Power } from "../../../core/models/power.model";
import { createActionType } from "../../shared/utils";

export const ADD_POWER = createActionType('ADD_POWER');
export const ADD_POWER_SUCCESS = createActionType('ADD_POWER_SUCCESS');
export const DELETE_POWER = createActionType('DELETE_POWER');
export const DELETE_POWER_SUCCESS = createActionType('DELETE_POWER_SUCCESS');
export const LOAD_POWERS = createActionType('LOAD_POWERS');
export const LOAD_POWERS_SUCCESS = createActionType('LOAD_POWERS_SUCCESS');
export const LOAD_POWER = createActionType('LOAD_POWER');
export const LOAD_POWER_SUCCESS = createActionType('LOAD_POWER_SUCCESS')
export const SELECT_POWER = createActionType('SELECT_POWER');
export const UPDATE_POWER = createActionType('UPDATE_POWER');
export const UPDATE_POWER_SUCCESS = createActionType('UPDATE_POWER_SUCCESS');

export class AddPower implements Action {
  readonly type = ADD_POWER;

  constructor(public payload: Power) {
  }
}

export class AddPowerSuccess implements Action {
  readonly type = ADD_POWER_SUCCESS;

  constructor(public payload: Power) {
  }
}

export class DeletePower implements Action {
  readonly type = DELETE_POWER;

  constructor(public payload: Power) {
  }
}

export class DeletePowerSuccess implements Action {
  readonly type = DELETE_POWER_SUCCESS;

  constructor(public payload: Power) {
  }
}

export class LoadPowers implements Action {
  readonly type = LOAD_POWERS;
}

export class LoadPowersSuccess implements Action {
  readonly type = LOAD_POWERS_SUCCESS;

  constructor(public payload: Power[]) {
  }
}

export class LoadPower implements Action {
  readonly type = LOAD_POWER;

  constructor(public payload: { id: number }) {
  }
}

export class LoadPowerSuccess implements Action {
  readonly type = LOAD_POWER_SUCCESS;

  constructor(public payload: Power) {
  }
}

export class SelectPower implements Action {
  readonly type = SELECT_POWER;

  constructor(public payload: { id: number }) {
  }
}

export class UpdatePower implements Action {
  readonly type = UPDATE_POWER;

  constructor(public payload: Power) {
  }
}

export class UpdatePowerSuccess implements Action {
  readonly type = UPDATE_POWER_SUCCESS;

  constructor(public payload: Power) {
  }
}

export type PowersAction =
  AddPower
  | AddPowerSuccess
  | DeletePower
  | DeletePowerSuccess
  | LoadPowers
  | LoadPowersSuccess
  | LoadPower
  | LoadPowerSuccess
  | SelectPower
  | UpdatePower
  | UpdatePowerSuccess;

Reducer

The next step of implementing the Redux pattern in our application is to define a reducer() function. If you recall from my post on the basics of Redux using NgRx in Angular, our reducer function is a pure function whose sole responsibility is to mutate the state of our application based on the action that is dispatched.

Create a new file src/app/state/powers/reducers/powers.ts:

import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { Power } from '../../../core/models/power.model';
import {
  ADD_POWER_SUCCESS,
  DELETE_POWER_SUCCESS,
  LOAD_POWER_SUCCESS,
  LOAD_POWERS_SUCCESS,
  PowersAction,
  SELECT_POWER,
  UPDATE_POWER_SUCCESS
} from '../actions/powers';

export interface State extends EntityState<Power> {
  selectedPowerId: number;
}

export const adapter: EntityAdapter<Power> = createEntityAdapter();

const initialState: State = adapter.getInitialState({
  selectedPowerId: null
});

export function reducer(state: State = initialState, action: PowersAction) {
  switch (action.type) {
    case ADD_POWER_SUCCESS:
      return adapter.addOne(action.payload, state);
    case DELETE_POWER_SUCCESS:
      return adapter.removeOne(action.payload.id, state);
    case LOAD_POWER_SUCCESS:
      return adapter.addOne(action.payload, state);
    case LOAD_POWERS_SUCCESS:
      return adapter.addMany(action.payload, state);
    case SELECT_POWER:
      return { ...state, selectedPowerId: action.payload.id };
    case UPDATE_POWER_SUCCESS:
      return adapter.updateOne(
        {
          id: action.payload.id,
          changes: action.payload
        },
        state
      );
    default:
      return state;
  }
}

export const getSelectedPowerId = (state: State) => state.selectedPowerId;

Before we dig into the code above, it is important to mention that we are using the new @ngrx/entity module that was recently released by the NgRx team. This module provides us with a simple interface for efficiently storing entities in our store.

The module also provides methods for easily updating the entities in our store:

Ok, let’s review the code above:

While we have defined our reducer() function, we still need to write this up to the root AppState. Before we dive into wiring up our reducer() function, let’s try to get a clear picture of what our store looks like:

Tour of Heroes Application

As you can see in the diagram above, the state of our application is similar to a tree structure:

This idea of having root state and then feature states is called fractal state management. According to the NgRx documentation:

Store uses fractal state management, which provides state composition through feature modules, loaded eagerly or lazily.

Ok, now let’s create the PowersState interface that will have a single powers property, which will be an object that has ids, entities and selectedPowerId properties.

Create a new file at src/app/state/powers/reducers/index.ts:

import { createFeatureSelector, createSelector } from '@ngrx/store';
import { AppState } from '../../app.interfaces';
import * as fromPowers from './powers';

export interface PowersState {
  powers: fromPowers.State;
}

export interface State extends AppState {
  powers: PowersState;
}

export const reducers = {
  powers: fromPowers.reducer
};

export const getPowersState = createFeatureSelector < PowersState > 'powers';

export const getPowersEntityState = createSelector(
  getPowersState,
  state => state.powers
);

export const {
  selectAll: getAllPowers,
  selectEntities: getPowerEntities,
  selectIds: getPowerIds,
  selectTotal: getPowersTotal
} = fromPowers.adapter.getSelectors(getPowersEntityState);

export const getSelectedPowerId = createSelector(
  getPowersEntityState,
  fromPowers.getSelectedPowerId
);

export const getSelectedPower = createSelector(
  getPowerEntities,
  getSelectedPowerId,
  (entities, selectedPowerId) => selectedPowerId && entities[selectedPowerId]
);

Let’s review:

To further explain some of the selector functions that are generated for us by the entity adapter:

The last step is to wire up the feature in our store. We’ll do this in the imports array of the StateModule:

import * as fromPowers from './powers/reducers';

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot(appReducer, {
      metaReducers: appMetaReducers,
    }),
    StoreModule.forFeature('powers', fromPowers.reducers),
    StoreRouterConnectingModule.forRoot(),
    EffectsModule.forRoot([AppEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: [],
})
export class StateModule {
  // code omitted
}

Note where we invoke the forFeature() static method on the StoreModule, specifying the feature name as the string powers. We also provide a reference to the reducer function for the feature.

If you compile your Angular application and load it in the browser, there should be no differences to the application, and viewing, adding, or updating powers should have no actions being dispatched in the Redux Chrome DevTools. You should also not have any exceptions in your console.

Effects

With our actions defined, and our reducer and selector functions defined, we are now ready to add a few effects. Remember, effects will respond to actions that are dispatched to the store and perform some side effect, usually an asynchronous side effect such as making an HTTP request.

Create a new file at src/app/state/powers/effects/powers.ts:

import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs/Observable";
import { map, switchMap } from "rxjs/operators";
import { PowersService } from "../../../core/services/powers.service";
import {
  ADD_POWER, AddPower, AddPowerSuccess, DELETE_POWER, DeletePower, DeletePowerSuccess, LOAD_POWER, LOAD_POWERS,
  LoadPower, LoadPowers, LoadPowersSuccess, LoadPowerSuccess, UPDATE_POWER, UpdatePower, UpdatePowerSuccess
} from "../actions/powers";


@Injectable()
export class PowersEffects {

  @Effect()
  addPower: Observable<Action> = this.actions.ofType<AddPower>(ADD_POWER)
    .pipe(
      map(action => action.payload),
      switchMap(power => this.powersService.createPower(power)),
      map(power => new AddPowerSuccess(power))
    );

  @Effect()
  deletePower: Observable<Action> = this.actions.ofType<DeletePower>(DELETE_POWER)
    .pipe(
      map(action => action.payload),
      switchMap(power => this.powersService.deletePower(power)),
      map(power => new DeletePowerSuccess(power))
    );

  @Effect()
  loadPowers: Observable<Action> = this.actions.ofType<LoadPowers>(LOAD_POWERS)
    .pipe(
      switchMap(() => this.powersService.getPowers()),
      map(powers => new LoadPowersSuccess(powers))
    );

  @Effect()
  loadPower: Observable<Action> = this.actions.ofType<LoadPower>(LOAD_POWER)
    .pipe(
      map(action => action.payload),
      switchMap(payload => this.powersService.getPower(payload.id)),
      map(power => new LoadPowerSuccess(power))
    );

  @Effect()
  updatePower: Observable<Action> = this.actions.ofType<UpdatePower>(UPDATE_POWER)
    .pipe(
      map(action => action.payload),
      switchMap(power => this.powersService.updatePower(power)),
      map(power => new UpdatePowerSuccess(power))
    );

  constructor(private actions: Actions,
              private powersService: PowersService) {
  }

}

We’ve defined all of the effects for our powers:

Let’s quickly review the addPower property:

As you can see, most of the effects follow a very similar pattern using the appropriate method in the PowersService to perform an asynchronous event. Finally, note that we inject both the Actions and PowersService in the class’s constructor() function.

Now, our class is ready to be added to our StateModule:

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot(appReducer, {
      metaReducers: appMetaReducers,
    }),
    StoreModule.forFeature('powers', fromPowers.reducers),
    StoreRouterConnectingModule.forRoot(),
    EffectsModule.forRoot([AppEffects]),
    EffectsModule.forFeature([PowersEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: [],
})
export class StateModule {
  // code omitted
}

We are now invoking the forFeature() static method on the EffectsModule. The argument to the method is an array of classes, and we specify our PowersEffects class. Also, note that this must come after we invoke the forRoot() method on the EffectsModule.

Again, building and loading our application in the browser should result in no changes to the application, and we should not see any actions being dispatched to the store when viewing, adding, removing, or updating the powers. Also, you should not have any exceptions in the console.

Refactor Index

Ok. With the actions, effects and reducers defined and configured for our store we are ready to start refactoring our application to use NgRx.

Here’s our current IndexComponent class:

export class IndexComponent implements OnInit {

  powers: Observable<Array<Power>>;

  // TODO: use store in place of service
  constructor(private matDialog: MatDialog, private powersService: PowersService) { }

  ngOnInit() {
    // TODO: dispatch action to load powers
    this.powers = this.powersService.getPowers();
  }

  // TODO: use store for dialog state
  // TODO: adding power doesn't emit new value in powers observable
  add() {
    this.matDialog.open(AddPowerDialogComponent);
  }

  delete(power: Power) {
    // TODO: use store for dispatching action
    this.powersService.deletePower(power)
      .subscribe(() => this.powers = this.powersService.getPowers());
  }

}

As you can see, I’ve added a bunch of TODO placeholders where we can improve our application by using NgRx. Let’s start refactoring the IndexComponent located at src/app/+powers/containers/index/index.component.ts:

export class IndexComponent implements OnInit {

  powers: Observable<Array<Power>>;

  constructor(private matDialog: MatDialog,
              private store: Store<PowersState>) {
  }

  ngOnInit() {
    this.powers = this.store.select(getAllPowers);
    this.store.dispatch(new LoadPowers());
  }

  add() {
    this.matDialog.open(AddPowerComponent);
  }

  delete(power: Power) {
    this.store.dispatch(new DeletePower(power));
  }

}

Here’s what we did:

Refactor Edit

Next, let’s refactor the container component that updates/mutates a Power object.

Our current EditComponent class is located at src/app/+powers/containers/edit/edit.component.ts:

export class EditComponent implements OnInit {

  power: Observable<Power>;

  // TODO: use store instead of service
  constructor(
    private activatedRoute: ActivatedRoute,
    private powersService: PowersService,
    private snackBar: MatSnackBar) {
  }

  ngOnInit() {
    // TODO: dispatch action to load power
    this.power = this.activatedRoute.paramMap
      .pipe(
        switchMap(paramMap => this.powersService.getPower(paramMap.get('id')))
      );
  }

  powerChange(power: Power) {
    // TODO: dispatch action to update power
    this.powersService.updatePower(power)
      .subscribe(() => {
        this.snackBar.open('Power Updated', 'Success', {
          duration: 2000
        });
      });
  }

}

Here’s our newly refactored component:

import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from "@angular/material";
import { ActivatedRoute } from "@angular/router";
import { Store } from "@ngrx/store";

import { Observable } from "rxjs/Observable";
import { first, map, switchMap, tap } from "rxjs/operators";

import { Power } from "../../../core/models/power.model";
import { LoadPower, SelectPower, UpdatePower } from "../../../state/powers/actions/powers";
import { getPowersTotal, getSelectedPower, PowersState } from "../../../state/powers/reducers";

@Component({
  selector: 'app-edit',
  templateUrl: './edit.component.html',
  styleUrls: ['./edit.component.scss']
})
export class EditComponent implements OnInit {

  power: Observable<Power>;

  constructor(private activatedRoute: ActivatedRoute,
              private snackBar: MatSnackBar,
              private store: Store<PowersState>) {
  }

  ngOnInit() {
    this.power = this.activatedRoute.paramMap
      .pipe(
        tap(paramMap => this.store.dispatch(new SelectPower({id: Number(paramMap.get('id'))}))),
        tap(paramMap => {
          this.hasPowersInStore()
            .subscribe(exists => {
              if (!exists) {
                this.store.dispatch(new LoadPower({id: Number(paramMap.get('id'))}));
              }
            });
        }),
        switchMap(() => this.store.select(getSelectedPower))
      );
  }

  hasPowersInStore(): Observable<boolean> {
    return this.store.select(getPowersTotal)
      .pipe(
        first(),
        map(total => total > 0)
      )
  }

  powerChange(power: Power) {
    this.store.dispatch(new UpdatePower(power));
    this.snackBar.open('Power Saved', 'Success', {
      duration: 2000
    });
  }
}

Let’s review the updated component code:

Here is a quick snapshot of loading and editing powers using NgRx:

Tour of Heroes Application

Refactor Add

The final step in our refactor is to update adding of powers to use the NgRx store.

The current AddComponentDialogComponent is located at src/app/+powers/components/add-power-dialog/add-power-dialog.component.ts:

export class AddPowerDialogComponent implements OnInit {

  form: FormGroup;

  // TODO: use store instead of service
  constructor(
    private formBuilder: FormBuilder,
    private matDialogRef: MatDialogRef<AddPowerDialogComponent>,
    private powersService: PowersService) {
  }

  // code omitted

  save() {
    if (!this.form.valid) {
      return;
    }

    // TODO: dispatch action to store
    this.powersService.createPower(this.form.value)
      .subscribe(() => this.close());
  }

}

Here is our newly refactored save() method:

export class AddPowerDialogComponent {
  save() {
    if (!this.form.valid) {
      return;
    }

    this.store.dispatch(new AddPower(this.form.value));
    this.close();
  }
}

I’ve omitted much of the existing code in the AddPowerDialogComponent for brevity. Here’s what we did:

Also, note that we close the dialog immediately after the user has clicked save. We will likely want to refactor this so that the dialog is only closed in the event that the save was successful. This is something that we can refactor so that opening and closing our dialog are actions, and as a result, we have effects that will invoke the dialog’s open() and close() methods.

Refactor Complete

I have an additional branch on the project in GitHub named ngrx-refactor-2 that includes all of the refactored code. Feel free to download or fork it:

Homework

While we have refactored the adding, editing, loading, and updating of powers to use the Redux pattern, the state of opening and closing the dialog to add a power is not being saved in our store. You may decide that that is ok for your application’s needs. But, you may want to store all application state in the store, including toggling the state of opening and closing the dialog to add a new power, as well as showing and hiding the snackbar.