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:
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:
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:
HeroesModule
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 thetype
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 actionstype
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
andLOAD_POWERS_SUCCESS
. - Next, we define the
LoadPowers
class. This class has the readonlytype
property required by theAction
interface. We set the value oftype
to the string constantLOAD_POWERS
. - Next, we define the
LoadPowersSuccess
class. Again, we specify a readonlytype
property that is set to the value of the string constant. Further, our class has aconstructor()
function that defines a publicpayload
property, whose type definition is an array ofPower
objects. - Finally, we define a new
PowersAction
type, which is either aLoadPowers
object or aLoadPowersSuccess
object. We use the TypeScript union operator to define the type. We will use thisPowersAction
in ourreducer()
function for type safety on theaction
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 theEntityState
interface, specifying thePower
model as the generic for the type of entities that our state will contain. - Next, using the
createEntityAdapter()
function we define theadapter
. - Then, we define the
initialState
of the store. We specify the initial state for theselectedPowerId
tonull
. - We are now ready to define the
reducer()
function. - Note that the default value of the
state
argument is theinitialState
object. - Also note that we specify the
action
type asPowersAction
. This will provide type safety when accessing an action’s payload property. - We switch on the
type
string property within theaction
. - 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 theSELECT_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 theselectedStateId
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 theselectedPowerId
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:
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 singlepowers
object. As our application grows, we can continue to add additional state objects to ourPowersState
as necessary. - Then, we define a new
State
interface that extends the rootAppState
interface. - We then define a
reducers
object that contains all of the reducers for thePowersState
. - 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 - thePowersState
. - We then create a
getPowersEntityState
selector function using thecreateSelector()
function. This will return the /store/powers/powers object - theState
. Note that the first argument to the selector is the feature selector, whose value is the first argument to theResult
function, which is the last argument to thecreateSelector()
function. - We then create four new selector functions:
getAllPowers()
,getPowerEntities()
,getPowerIds()
andgetPowersTotal()
. These selector functions are generated by the entity adapter via thegetSelectors()
method. Note that we provide the parentgetPowersEntityState
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 theselectedPowerId
number value. - Finally, we create a
getSelectedPower()
selector function to obtain the selectedPower
entity.
To further explain some of the selector functions that are generated for us by the entity adapter:
getAllPowers()
returns an array ofPower
entities sorted based on theids
array.getPowerEntities()
returns a dictionary ofPower
entities.getPowerIds()
returns an array of number values that uniqely identify each entity in theentities
dictionary.getPowersTotal()
returns the total number ofPower
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 ourPowersEffects
class with a return type of anObservable
of anAction
. 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 theADD_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 thepayload
object from the action. In this case, the payload is aPower
object. - Then, we use the
switchMap()
operator, which receives thePower
object that our previousmap()
operator returned. We then return theObservable
that is returned from thecreatePower()
method in thePowersService
. This is anObservable
of the newly createdPower
object. - Then, we use the
map()
operator to return the newAddPowerSuccess
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 theconstructor()
function as we’ll be using theStore
. - We then import the
Store
in theconstructor()
function, specifying thePowersState
interface as the generic type. - In the
ngOnInit()
lifecycle method we set thepowers
public property using thegetAllPowers
selector. We pass a reference to the selector function to theselect()
method onStore
. Then, wedispatch()
theLoadPowers
action to theStore
. - When the user deletes a power, the
delete()
method is invoked from an output binding on the child component. In thedelete()
method we are now dispatching theDeletePower
action to the store, providing thePower
object as the payload. - Previously, we were using the
PowersService
to delete a specificPower
object. We were also performing an additional HTTP request to obtain the updated list ofPower
objects in the view. This is a benefit of using the Redux Pattern. When we remove thePower
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 theconstructor()
function. - We are going to need the
Store
, so inject that via theconstructor()
function. - In the
ngOnInit()
lifecycle method we want to get the selectedPower
object based on theid
paramater in our route. We’ll us theparamMap
observable on theactivatedRoute
instance that is injected into theconstructor()
function. First, we dispatch theSelectPower
action to update theselectedPowerId
property in the store. Then, we check if we have anyPower
entities in the store. If not, we’ll dispatch theLoadPower
action to load the selected power. Finally, we’ll return the observable of the selectedPower
object using theselect()
method on theStore
, providing thegetSelectedPower
selector function. - The
hasPowersInStore()
returns anObservable
of aboolean
value, indicating if there are anyPower
objects in the store. We use thegetPowersTotal
selector function to obtain the count of the entities. We use thefirst()
operator because we are only concerned with the first value that is emitted from the observable. We thenmap()
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 thedebounceTime()
method to prevent the event from being emitted on each and every key stroke. To update the power, we simplydispatch()
theUpdatePower
action, providing thePower
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:
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 theconstructor()
function. - Finally, we update the
save()
method to dispatch theAddPower
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.