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

Angular + NgRx: Refactor Module

Reading time ~21 minutes

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:

  • Angular 5.0.2
  • RxJS 5.5.2
  • TypeScript 2.4.2

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:

  • First, we import the Action interface. This interface requires that our action objects have the type property. While an action doesn’t need to be defined as a class, and we can dispatch actions to our store by simply providing the action object, using a class provides the added benefit of type safety.
  • Next, we import the Power model.
  • Then, we import the createActionType function. This is not necessary for your application, however this can be helpful as it ensures that your actions type strings are unique. This function will throw a runtime exception if you reuse the same string twice, helping you to avoid some odd behavior that will result if two actions use the same string value.
  • We then define two string constants: LOAD_POWERS and LOAD_POWERS_SUCCESS.
  • Next, we define the LoadPowers class. This class has the readonly type property required by the Action interface. We set the value of type to the string constant LOAD_POWERS.
  • Next, we define the LoadPowersSuccess class. Again, we specify a readonly type property that is set to the value of the string constant. Further, our class has a constructor() function that defines a public payload property, whose type definition is an array of Power objects.
  • Finally, we define a new PowersAction type, which is either a LoadPowers object or a LoadPowersSuccess object. We use the TypeScript union operator to define the type. We will use this PowersAction in our reducer() function for type safety on the action argument to the reducer.

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

  • Adding powers
  • Deleting powers
  • Loading a single power
  • Selecting a power
  • Updating a power

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:

  • addOne()
  • addMany()
  • addAll()
  • removeOne()
  • removeMany()
  • removeAll()
  • updateOne()
  • updateMany()

Ok, let’s review the code above:

  • First, import the necessary classes and function from the @ngrx/entity module. We then import the Power model, as well as the actions that we previously defined.
  • Next, we define the State interface the extends the EntityState interface, specifying the Power model as the generic for the type of entities that our state will contain.
  • Next, using the createEntityAdapter() function we define the adapter.
  • Then, we define the initialState of the store. We specify the initial state for the selectedPowerId to null.
  • We are now ready to define the reducer() function.
  • Note that the default value of the state argument is the initialState object.
  • Also note that we specify the action type as PowersAction. This will provide type safety when accessing an action’s payload property.
  • We switch on the type string property within the action.
  • For each action we invoke the appropriate adapter method to mutating the state of our application.
  • Note that we have to manually update the state object for the SELECT_STATE action as there is not a built-in method on the adapter for this. In this case we use the spread operator (the leading three dots) to make a copy of the state object, and then we overwrite the value of the selectedStateId property.
  • Also note that we have a default case where we return the state that was provided, making no changes to the state of our application. Don’t forget to do this, otherwise, you’ll see some odd behavior in your application, likely an exception indicating that the state is undefined.
  • Finally, we export a getSelectedPowerId() function that accepts the state and returns the selectedPowerId property value.

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:

  • /store (AppState)
  • /store/powers (PowerState)
  • /store/powers/powers (State)
  • /store/powers/powers/ids (array of id strings or numbers)
  • /store/powers/powers/entities (dictionary of Power entities)
  • /store/powers/powers/seletedPowerId (the selected power id number)

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:

  • First, we import the necessary functions from the NgRx @ngrx/store module.
  • Then, we import our root AppState interface.
  • Then, we import all of the exported values from the src/app/state/powers/reducers/powers.ts file we just created.
  • Next we define a new PowersState interface. Our state currently has a single powers object. As our application grows, we can continue to add additional state objects to our PowersState as necessary.
  • Then, we define a new State interface that extends the root AppState interface.
  • We then define a reducers object that contains all of the reducers for the PowersState.
  • Note that the property for each object and interface is powers. This is important, as they have to match.
  • We are now ready to create our feature selector using the createFeatureSelector() function. This will return the /store/powers object - the PowersState.
  • We then create a getPowersEntityState selector function using the createSelector() function. This will return the /store/powers/powers object - the State. Note that the first argument to the selector is the feature selector, whose value is the first argument to the Result function, which is the last argument to the createSelector() function.
  • We then create four new selector functions: getAllPowers(), getPowerEntities(), getPowerIds() and getPowersTotal(). These selector functions are generated by the entity adapter via the getSelectors() method. Note that we provide the parent getPowersEntityState selector function.
  • Also note that when we are creating selector functions that we provide parent selector functions, to sort of roll up our selectors. Note that we do not invoke those funtions, rather, we provide a reference to the function and allow NgRx to invoke those functions for us.
  • We then create a getSelectedPowerId() selector function to obtain the selectedPowerId number value.
  • Finally, we create a getSelectedPower() selector function to obtain the selected Power entity.

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

  • getAllPowers() returns an array of Power entities sorted based on the ids array.
  • getPowerEntities() returns a dictionary of Power entities.
  • getPowerIds() returns an array of number values that uniqely identify each entity in the entities dictionary.
  • getPowersTotal() returns the total number of Power entities that are in the store.

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:

  • addPower
  • deletePower
  • loadPowers
  • loadPower
  • updatePower

Let’s quickly review the addPower property:

  • First, we define the addPower property in our PowersEffects class with a return type of an Observable of an Action. All of our effects will perform a side effect and then return a new action to be dispatched to the store.
  • We use the ofType() method to filter the actions that are dispatched so that our effect is only executed for the specific action we want to perform the effect for. In this case, we are only going to perform the effect when the ADD_POWER action is dispatched.
  • We then use the pipe() method that was introduced in RxJS v5.5.x. This method accepts a variable number of operators that receive the value emited from the previous operator in the chain. This enables us to chain together operators.
  • We first use the map() operator to get the payload object from the action. In this case, the payload is a Power object.
  • Then, we use the switchMap() operator, which receives the Power object that our previous map() operator returned. We then return the Observable that is returned from the createPower() method in the PowersService. This is an Observable of the newly created Power object.
  • Then, we use the map() operator to return the new AddPowerSuccess action object.

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:

  • First, we no longer need to import the PowersService in the constructor() function as we’ll be using the Store.
  • We then import the Store in the constructor() function, specifying the PowersState interface as the generic type.
  • In the ngOnInit() lifecycle method we set the powers public property using the getAllPowers selector. We pass a reference to the selector function to the select() method on Store. Then, we dispatch() the LoadPowers action to the Store.
  • When the user deletes a power, the delete() method is invoked from an output binding on the child component. In the delete() method we are now dispatching the DeletePower action to the store, providing the Power object as the payload.
  • Previously, we were using the PowersService to delete a specific Power object. We were also performing an additional HTTP request to obtain the updated list of Power objects in the view. This is a benefit of using the Redux Pattern. When we remove the Power object from the store, all observers will receive the new value that is emitted from the observable.

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:

  • First, we are no longer going to use the PowersService in our component, so we can remove this from the constructor() function.
  • We are going to need the Store, so inject that via the constructor() function.
  • In the ngOnInit() lifecycle method we want to get the selected Power object based on the id paramater in our route. We’ll us the paramMap observable on the activatedRoute instance that is injected into the constructor() function. First, we dispatch the SelectPower action to update the selectedPowerId property in the store. Then, we check if we have any Power entities in the store. If not, we’ll dispatch the LoadPower action to load the selected power. Finally, we’ll return the observable of the selected Power object using the select() method on the Store, providing the getSelectedPower selector function.
  • The hasPowersInStore() returns an Observable of a boolean value, indicating if there are any Power objects in the store. We use the getPowersTotal selector function to obtain the count of the entities. We use the first() operator because we are only concerned with the first value that is emitted from the observable. We then map() the count to a boolean value that is returned.
  • The powerChange() method is called by an output binding to a child component. This method is called whenever we have a change to the power’s name property. If you look in the child component, you’ll notice that we use the debounceTime() method to prevent the event from being emitted on each and every key stroke. To update the power, we simply dispatch() the UpdatePower action, providing the Power object as the payload.
  • Finally, we are currently showing a snackbar to the user. This snackbar is a bit misleading, as we show it no matter if the power was updated successfully, or not. We’ll want to refactor this into actions and effects so that showing and hiding the snackbar is handled as a result of the success or failure of our effect that invokes our REST API via an HTTP request.

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:

  • First, we remove the PowerService as we are no longer going to need it.
  • Then, we inject the Store via the constructor() function.
  • Finally, we update the save() method to dispatch the AddPower action to the store.

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.

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).