Enough talk! Let’s see some action … and reducer (pun intended!)

We’ll continue from where we left off in Part 3 (If you are new to this series,you can get the source from here - repo link)

Please note that “forRoot()” was deprecated and is now removed in angular/flex-layout — https://github.com/angular/flex-layout/blob/master/CHANGELOG.md. If you’re using 2.0.0-beta.4 or higher change the app.module.ts and remove .forRoot() for FlexLayoutModule “forRoot()” has also been deprecated for angular/material- https://github.com/angular/material2/blob/master/CHANGELOG.md. If you’re using 2.0.0-beta.2 or higher change the app.module.ts and remove .forRoot() for MaterialModule. In addition, the material prefix “md-” for styles is changed to “mat-” and md-input, md-prefix & md-suffix are changed to mdInput, mdPrefix & mdSuffix respectively. I’ve upgraded the part-4 repo to part-4.1 to reflect these changes. If you’re using an earlier version of the rwa-trivia repo with the new version of these libraries, please make appropriate changes to your local projects.

For our Angular application, we’re going to use the ngrx library to provide us with our state management using these patterns. You can see the comprehensive introduction to ngrx - https://gist.github.com/btroncone/a6e4347326749f938510 and checkout the github repo. ngrx/store provides us with actions & reducers, while ngrx/effects gives us the ability to inject middlewares.

Let’s add this to our project -

npm install --save @ngrx/core @ngrx/effects @ngrx/store

We’ll create a new folder store under src/app and add actions, reducers and effects folders under it and add app-store.ts to the store. The folder structure will look something like this.

Let’s start with Category action, reducer and effect (imports statements may be excluded for brevity) -

// category.actions.ts import {Injectable} from '@angular/core';

import {Action} from '@ngrx/store';

import {Category} from '../../model'; @Injectable()

export class CategoryActions { static LOAD_CATEGORIES = 'LOAD_CATEGORIES';

loadCategories(): Action {

return {

type: CategoryActions.LOAD_CATEGORIES

};

} static LOAD_CATEGORIES_SUCCESS = 'LOAD_CATEGORIES_SUCCESS';

loadCategoriesSuccess(categories: Category[]): Action {

return {

type: CategoryActions.LOAD_CATEGORIES_SUCCESS,

payload: categories

};

} }

We introduce 2 actions, LOAD_CATEGORIES to initiate the load and LOAD_CATEGORIES_SUCCESS, which would be invoked when our service returns data successfully. (We’ll add error cases later)

// categories.reducer.ts import { Observable } from 'rxjs/Observable';

import '../../rxjs-extensions';

import {Action} from '@ngrx/store';

import {CategoryActions} from '../actions';

import { Category } from '../../model'; export const categories = (state: any = [], action: Action): Category[] => {

switch (action.type) {

case CategoryActions.LOAD_CATEGORIES_SUCCESS:

return action.payload;

default:

return state;

}

};

The reducer looks for LOAD_CATEGORIES_SUCCESS action and returns payload as the new state.

Note the arguments provided to a reducer are the state and action (action has type and payload). During initialization, the value of the reducer set to the default value provided with the state. In our case, it’ll be an empty array.

// category.effects.ts import {Injectable} from '@angular/core';

import {Effect, Actions} from '@ngrx/effects'; import ... ...

@Injectable()

export class CategoryEffects {

constructor (

private actions$: Actions,

private categoryActions: CategoryActions,

private svc: CategoryService

) {} @Effect()

loadCategories$ = this.actions$

.ofType(CategoryActions.LOAD_CATEGORIES)

.switchMap(() => this.svc.getCategories())

.map((categories: Category[]) => this.categoryActions.loadCategoriesSuccess(categories))

}

The CategoryEffects middleware listens for LOAD_CATEGORIES action, injects the service call to getCategories and then calls the LOAD_CATEGORIES_SUCCESS action on it’s completion, passing the categories list as the payload.

// app.store.ts import { Category } from '../model'; import { categories } from './reducers'; import { combineReducers } from '@ngrx/store';

import { compose } from '@ngrx/core/compose'; export interface AppStore {

categories: Category[];

} export default compose(combineReducers)({

categories: categories

});

And finally we add our categories reducer to our store. Make sure you export your actions, reducers and effects using index.ts in the respective folders.

This completes our store for categories. We now need to wire it up in our app.module.ts.

Let’s switch our categories component to subscribe to the store instead of the service.

// categories.component.ts export class CategoriesComponent implements OnInit {

categoriesObs: Observable<Category[]>;

categories: Category[];

sub: any; constructor(private store: Store<AppStore>) {

this.categoriesObs = store.select(s => s.categories);

} ngOnInit() {

this.sub = this.categoriesObs.subscribe(categories => this.categories = categories);

} ngOnDestroy() {

if (this.sub)

this.sub.unsubscribe();

}

}

And finally we’ll dispatch our action to load the categories. We’ll do this in our app.component as we would need to load the categories just once and use it in different parts of our application (the categories data is pretty much static thru the lifetime of our SPA).

Great! Let’s serve it up & try it out. Notice that the categories api is called only once even when we navigate away from our categories route and back to it. However, our questions route still invokes the service every time as it’s still making calls to the service and not utilizing the store. See commit for intermediate code.