Bonus : Working with NgRx
state challenges
application state is lost when Angular app is reloaded
using services and subjects to keep everything updated can be complicated as the app grows
Redux is from ngrx
provides a Store, a single place to manage application state
Services and Components are in the receive state meaning they can get values from the Store
to set values in the Store, Services and Components dispatch Actions
Actions are sent to a Reducer which reduces/combines state
Reducers are functions which take Action and potential payload as args
the result is passed to the Store
NgRx supports this behavior

Top

Index

getting started with reducers
to install NgRx
    npm install --save @ngrx/store
        
this will not work when using the combination of Windows 10, npm v3.10.10, and node v6.11.1
add a folder named store to shopping-list folder
create new file named shopping-list.reducers.ts
ngrx passes the arguments to the ShoppingListReducer
initialState provides a state when ngrx provides no state
reducers are used to update the state
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';

    const initialState {
        ingredients: [
            new Ingredient('Apples', 5),
            new Ingredient('Tomatoes', 10),
        ]
    };

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';

    export function ShoppingListReducer(state = initialState, action: Action) {
        switch (action.type) {
            case ADD_INGREDIENT:
                // spread operator expands existing array's elements
                return { ...state, ingredients: [...state.ingredients, action.payload] };
            default:
                return state;
        }
    }
        
need to create an Action to call the reducer

Top

Index

adding actions
add shopping-list.actions.ts to shared folder
move ADD_INGREDIENT constant from shopping-list.reducers to new file
add class AddIngredient implementing the Action interface
set type property
add payload property
add container to hold all actions
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';

    export class AddIngredient implements Action {
        readonly type = ADD_INGREDIENT;
        payload: Ingredient;
    }

    export type ShoppingListActions = AddIngredient;
        

Top

Index

finishing the first reducer
returning to the reducer import everything from the shopping-list.actions file
change the action arg to type ShoppingListActionsContainer.AddIngredient
fix the case statement to use the constant from the action file
add the action's payload to the new state object
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';
    import * as ShoppingListActionsContainer from './shopping-list.actions';

    const initialState = {
        ingredients: [
            new Ingredient('Apples', 5),
            new Ingredient('Tomatoes', 10),
        ]
    };

    // ngrx passes the arguments
    export function ShoppingListReducer(state = initialState, action: ShoppingListActionsContainer.ShoppingListActions) {
        switch (action.type) {
            case ShoppingListActionsContainer.ADD_INGREDIENT:
                // spread operator expands existing array's elements
                return { ...state, ingredients: [...state.ingredients, action.payload] };
            default:
                return state;
        }
    } 
        

Top

Index

registering the application store
in app.module add import statements for the StoreModule and the ShoppingListReducers
in imports property add the StoreModule using its forRoot method to register the Store and its reducers
    ...
    import { StoreModule } from '@ngrx/store';
    ...
    import { ShoppingListReducer } from './shopping-list/store/shopping-list.reducers';

    @NgModule({
      declarations: [
        AppComponent,
      ],
      imports: [
        ...
        StoreModule.forRoot({ shoppingList: ShoppingListReducer })
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
        

Top

Index

selecting data from the store
inject ngrx Store into shopping-list.component
add import statements for Store and Observable
change the name of the ingredients array to shoppingListState and change its type an object containing an array of Ingredients
in ngOnInit comment out existing code and use Store's select method to get an observable containing an array of Ingredients
    import { Component, OnDestroy, OnInit } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { Subscription } from 'rxjs/Subscription';
    import { Observable } from 'rxjs/Observable';

    import { Ingredient } from '../shared/ingredient.model';
    import { ShoppingListService } from './shopping-list.service';

    @Component({
      ...
    })
    export class ShoppingListComponent implements OnInit {
      shoppingListState: Observable<{ ingredients:=ingredients Ingredient[]=Ingredient[] }=}>;
      subscription: Subscription;

      constructor(private shoppingListService: ShoppingListService, private store: Store<{ shoppingList:=shoppingList {={ ingredients:=ingredients Ingredient[]=Ingredient[] }=} }=}>) { }

      ngOnInit() {
        this.shoppingListState = this.store.select('shoppingList');
      }

      onEditItem(index: number) {
        this.shoppingListService.startedEditing.next(index);
      }
    }
        
in the shopping-list.component markup change the ngFor expression to use the async pipe to iterate the array of ingredients contained by the shoppingListState Observable
    <ul class="list-group">
        <a
        class="list-group-item"
        style="cursor: pointer"
        *ngFor="let ingredient of (shoppingListState | async).ingredients; let i = index"
        (click)="onEditItem(i)"
        >
        {{ ingredient.name }} ({{ ingredient.amount }})
        </a>
    </ul>
        

Top

Index

dispatch actions
in shopping-list.actions remove the payload property and add a c'tor which takes and ingredient as an argument
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';

    export class AddIngredient implements Action {
        readonly type = ADD_INGREDIENT;
        constructor(public payload: Ingredient) {}
    }

    export type ShoppingListActions = AddIngredient;
        
in the shopping-edit component inject the Store
in the onAddItem method use the store's dispatch method using a new AddIngredient action as an arg
    ...
    export class ShoppingEditComponent implements OnDestroy, OnInit {
      ...
      constructor(private shoppingListService: ShoppingListService, private store: Store<{shoppingList: {ingrdients:={ingrdients Ingredient[]}}=Ingredient[]}}>) { }
      ...
      onAddItem(form: NgForm) {
        const value = form.value;
        const newIngredient = new Ingredient(value.name, value.amount);
        if (this.editMode) {
          this.shoppingListService.updateIngredient(this.editedItemIndex, newIngredient);
        } else {
          // this.shoppingListService.addIngredient(newIngredient);
          this.store.dispatch(new ShoppingListActionsContainer.AddIngredient(newIngredient));
        }
        this.editMode = false;
        form.reset();
      }
      ...
    }
        

Top

Index

more actions and adding ingredients
in shopping-list.actions add an action named AddIngredients
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';
    export const ADD_INGREDIENTS = 'ADD_INGREDIENTS';

    export class AddIngredient implements Action {
        readonly type = ADD_INGREDIENT;
        constructor(public payload: Ingredient) {}
    }

    export class AddIngredients implements Action {
        readonly type = ADD_INGREDIENTS;
        constructor(public payload: Ingredient[]) {}
    }
    export type ShoppingListActions = AddIngredient | AddIngredients;
        
in shopping-list.reducers add a case for the new action
     import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';
    import * as ShoppingListActionsContainer from './shopping-list.actions';

    const initialState = {
        ingredients: [
            new Ingredient('Apples', 5),
            new Ingredient('Tomatoes', 10),
        ]
    };

    // ngrx passes the arguments
    export function ShoppingListReducer(state = initialState, action: ShoppingListActionsContainer.ShoppingListActions) {
        switch (action.type) {
            case ShoppingListActionsContainer.ADD_INGREDIENT:
                // spread operator expands existing array's elements
                return { ...state, ingredients: [...state.ingredients, action.payload] };
            case ShoppingListActionsContainer.ADD_INGREDIENTS:
                return { ...state, ingredients: [...state.ingredients, ...action.payload] };
            default:
                return state;
        }
    }
        
in the RecipeDetailComponent inject the Store
in onAddToShoppingList use the store's dispatch method with a new AddIngredients action as an arg
    import { Store } from '@ngrx/store';
    ...
    import * as ShoppingListActionsContainer from '../../shopping-list/store/shopping-list.actions';
    ...
    @Component({
      ...
    })
    export class RecipeDetailComponent implements OnInit {
      recipe: Recipe;
      id: number;

      constructor(
        private recipeService: RecipeService,
        private route: ActivatedRoute,
        private router: Router,
        private store: Store<{ shoppingList:=shoppingList {={ ingredients:=ingredients Ingredient[]=Ingredient[] }=} }=}>) { }

      ...
      onAddToShoppingList() {
        // this.recipeService.addIngredientsToShoppingList(this.recipe.ingredients);
        this.store.dispatch(new ShoppingListActionsContainer.AddIngredients(this.recipe.ingredients));
      }
      ...
    }
        

Top

Index

dispatching update and deleting shopping list actions
in shopping-list.actions add Actions and constants for update and delete
note: every action has a public property named payload
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';
    export const ADD_INGREDIENTS = 'ADD_INGREDIENTS';
    export const UPDATE_INGREDIENT = 'UPDATE_INGREDIENT';
    export const DELETE_INGREDIENT = 'DELETE_INGREDIENT';

    export class AddIngredient implements Action {
        readonly type = ADD_INGREDIENT;
        constructor(public payload: Ingredient) { }
    }

    export class AddIngredients implements Action {
        readonly type = ADD_INGREDIENTS;
        constructor(public payload: Ingredient[]) { }
    }

    export class UpdateIngredient implements Action {
        readonly type = UPDATE_INGREDIENT;
        constructor(public payload: { index: number, ingredient: Ingredient }) { }
    }
    export class DeleteIngredient implements Action {
        readonly type = DELETE_INGREDIENT;
        constructor(public payload: number) { }
    }

    export type ShoppingListActions =
        AddIngredient |
        AddIngredients |
        UpdateIngredient |
        DeleteIngredient;
        
in the ShoppingListReducer add cases for the update and delete actions
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';
    import * as ShoppingListActionsContainer from './shopping-list.actions';

    const initialState = {
        ingredients: [
            new Ingredient('Apples', 5),
            new Ingredient('Tomatoes', 10),
        ]
    };

    // ngrx passes the arguments
    export function ShoppingListReducer(state = initialState, action: ShoppingListActionsContainer.ShoppingListActions) {
        let ingredients;
        switch (action.type) {
            case ShoppingListActionsContainer.ADD_INGREDIENT:
                // spread operator expands existing array's elements
                return { ...state, ingredients: [...state.ingredients, action.payload] };
            case ShoppingListActionsContainer.ADD_INGREDIENTS:
                return { ...state, ingredients: [...state.ingredients, ...action.payload] };
            case ShoppingListActionsContainer.UPDATE_INGREDIENT:
                const ingredient = state.ingredients[action.payload.index];
                const updatedIngredient = {
                    ...ingredient,
                    ...action.payload.ingredient
                };
                ingredients = [...state.ingredients];
                ingredients[action.payload.index] = updatedIngredient;
                return { ...state, ingredients: ingredients };
            case ShoppingListActionsContainer.DELETE_INGREDIENT:
                ingredients = [...state.ingredients];
                ingredients.splice(action.payload, 1);
                return { ...state, ingredients: ingredients };
            default:
                return state;
        }
    }
        
in the ShoppingEditComponent change onAddIetm and onDelete as shown below
     ...
    export class ShoppingEditComponent implements OnDestroy, OnInit {
      subscription: Subscription;
      editMode = false;
      editedItemIndex: number;
      editedIngredient: Ingredient;
      @ViewChild('f') slForm: NgForm;

      ...
      onAddItem(form: NgForm) {
        const value = form.value;
        const newIngredient = new Ingredient(value.name, value.amount);
        if (this.editMode) {
          // this.shoppingListService.updateIngredient(this.editedItemIndex, newIngredient);
          const payload = { ingredient: newIngredient, index: this.editedItemIndex };
          this.store.dispatch(new ShoppingListActionsContainer.UpdateIngredient(payload));
        } else {
          this.store.dispatch(new ShoppingListActionsContainer.AddIngredient(newIngredient));
        }
        this.editMode = false;
        form.reset();
      }
      ...  
      onDelete() {
        // this.shoppingListService.deleteIngredient(this.editedItemIndex);
        this.store.dispatch(new ShoppingListActionsContainer.DeleteIngredient(this.editedItemIndex));
        this.onClear();
      }
    }
        

Top

Index

expanding app state
in shopping-list.reducers add editedIngredient and editedIngredientIndex as properties of the initialState JSON data object
add a ShoppingListState interface which matches the initialState object and have the initialState object implement the interface
add a second interface AppState with a property of type ShoppingListState
    import { Action } from '@ngrx/store';

    import { Ingredient } from '../../shared/ingredient.model';
    import * as ShoppingListActionsContainer from './shopping-list.actions';

    export interface AppState {
        shoppingList: ShoppingListState;
    }
    export interface ShoppingListState {
        ingredients: Ingredient[];
        editedIngredient: Ingredient;
        editIngredientIndex: number;
    }

    const initialState: ShoppingListState = {
        ingredients: [
            new Ingredient('Apples', 5),
            new Ingredient('Tomatoes', 10),
        ],
        editedIngredient: null,
        editIngredientIndex: -1
    };
        
doing this now simplifies c'tors of the type which take the Store as an arg
    ...
    import * as ShoppingListReducersContainer from '../../shopping-list/store/shopping-list.reducers';
    ...
    export class RecipeDetailComponent implements OnInit {
      ...
      constructor(
        private recipeService: RecipeService,
        private route: ActivatedRoute,
        private router: Router,
        private store: Store<shoppinglistreducerscontainer.appstate>) { }
      ...
    }
        

Top

Index

authentication and side effects - introduction
async operations can't be done inside of reducers
reducers must return state immediately

Top

Index

a closer look at effects
an effect is something which causes the state to be changed
below the call to Firebase is an effect
    signupUser(email: string, password: string) {
        firebase.auth().createUserWithEmailAndPassword(email, password)
            .then(user => {
                this.store.dispatch(new AuthActions.Signup());
                this.router.navigate(['/']);
                firebase.auth().currentUser.getIdToken()
                    .then((token: string) => {
                        this.store.dispatch(new AuthActions.SetToken(token));
                    });
            })
            .catch(
            error => console.log(error)
            );
    }
        
install @ngrx/effects package
    npm install --save @ngrx/effects
        

Top

Index

auth effects and actions
in auth.effects import @ngrx/effects and create a class names AuthEffects
add import statement for Actions and Effect from @ngrx/effects
inject Actions into AuthEffects
add import for Injectable from @angular/core
decorate class with Injectable
    import { Actions, Effect } from '@ngrx/effects';
    import { Injectable } from '@angular/core';

    @Injectable()
    export class AuthEffects {
        // Actions is an Observable
        constructor(private actions$: Actions) {}
    }
        
register the EffectsModule in app.module
forRoot registers the Effect types to be used
    ...
    import { EffectsModule } from '@ngrx/effects';
    ...
    import { AuthEffects } from './auth/store/auth.effects';

    @NgModule({
      declarations: [
        AppComponent,
      ],
      imports: [
        ...
        EffectsModule.forRoot([AuthEffects])
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
        

Top

Index

effects - how they work
in auth.actions add a TrySignup Action
    import { Action } from '@ngrx/store';

    export const TRY_SIGNUP = 'TRY_SIGNUP';
    ...

    export class TrySignup implements Action {
        readonly type = TRY_SIGNUP;

        constructor(public payload: {username: string, password: string }) {}
    }
    ...
    export type AuthActions =
        ...|
        SetToken |
        TrySignup;
        
in auth.effects add import statement for auth.actions
add authSignup property and decorate with @Effect()
any time the TrySignup action happens methods chained to the ofType method will be executed
    import { Actions, Effect } from '@ngrx/effects';
    import { Injectable } from '@angular/core';

    import * as fromAuth from './auth.actions';

    @Injectable()
    export class AuthEffects {
        // decorate property
        @Effect()
        authSignup = this.actions$
            .ofType(fromAuth.TRY_SIGNUP);

        // Actions is an Observable
        constructor(private actions$: Actions) { }
    }
        
in sign-up.component remove the AuthService injection
inject the Store into the type
in the onSignup method use the store to fire the TrySignup action

        
    ...
    import { Store } from '@ngrx/store';

    // import { AuthService } from '../auth.service';
    import * as fromApp from '../../store/app.reducers';
    import * as AuthActions from '../store/auth.actions';

    @Component({
      ...
    })
    export class SignUpComponent implements OnInit {

      // constructor(private authService: AuthService) { }
      constructor(private store: Store<fromApp.AppState>) { }
      ...
      onSignup(form: NgForm) {
        const email = form.value.email;
        const password = form.value.password;
       // this.authService.signupUser(email, password);
        this.store.dispatch(new AuthActions.TrySignup({username: email, password: password}));
      }
    }
        

Top

Index

adding auth signup
every time the auth signup action occurs ngrx/effects will call the chained methods showed below
each method in the chain returns an observable
the router was injected in order to move the user from the signup page to the home view
    import { Actions, Effect } from '@ngrx/effects';
    import { Injectable } from '@angular/core';
    import 'rxjs/add/operator/map';
    import 'rxjs/add/operator/switchMap';
    import 'rxjs/add/operator/mergeMap';
    import * as firebase from 'firebase';
    import { fromPromise } from 'rxjs/observable/fromPromise';
    import { Router } from '@angular/router';

    import * as AuthActions from './auth.actions';
    import * as fromAuth from './auth.reducers';

    @Injectable()
    export class AuthEffects {
        // decorate property
        @Effect()
        // chained methods return observables
        authSignup = this.actions$
            .ofType(AuthActions.TRY_SIGNUP)
            // returns the username and passwords in an observable
            .map((action: AuthActions.TrySignup) => {
                return action.payload;
            })
            // fromPromise converts a Promise into an Observable
            .switchMap((authData: { username: string, password: string }) => {
                return fromPromise(firebase.auth().createUserWithEmailAndPassword(authData.username, authData.password));
            // don't need value returned by previous method in the chain
            }).switchMap(() => {
                return fromPromise(firebase.auth().currentUser.getIdToken());
            })
            // map multiple observables into one
            .mergeMap((token: string) => {
                this.router.navigate(['/']);
                return [
                    {
                        type: AuthActions.SIGNUP
                    },
                    {
                        type: AuthActions.SET_TOKEN,
                        payload: token
                    }
                ];
            });

        // Actions is like an Observable
        constructor(private actions$: Actions, private router: Router) { }
    }
        

Top

Index

the router store package
install router-store property
            npm install --save @ngrx/router-store
        
in app.module add import statement for StoreRouterConnectingModule
add StoreRouterConnectingModule to the module's imports property
    ...
    import { StoreRouterConnectingModule} from '@ngrx/router-store';
    ...
    @NgModule({
      declarations: [
        AppComponent,
      ],
      imports: [
        ...
        StoreRouterConnectingModule
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
        

Top

Index

store dev tools
install package
            npm install --save @ngrx/store-devtools
        
need to use Chrome extension redux devtools from Chrome web store
in app.module add import statement for StoreDevtoolsModule
add StoreDevtoolsModule to the module's imports property
add import statement for environment
environment has one property, a boolean named production
production is false in environment.ts and true in environment.prod.ts
use the property in a ternary statement to determine if StoreDevtoolsModule should be added to the module's import property
    ...
    import { StoreDevtoolsModule } from '@ngrx/store-devtools';
    import { environment } from '../environments/environment';
    ...
    @NgModule({
      declarations: [
        AppComponent,
      ],
      imports: [
        ...
        StoreRouterConnectingModule,
        !environment.production ? StoreDevtoolsModule.instrument() : []
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
        
using redux from Chrome's dev tools provides insights into how the application works

Top

Index

lazy load and dynamic injection
because recipes module is lazily loaded can't be included in state store because the code is not available
can dynamically inject new variable into store
create store folder under recipes directory
in folder add recipes.reducers.ts
export the reducer method and add the initial state value and the two interfaces
    import { Ingredient } from '../../shared/ingredient.model';
    import {Recipe } from '../recipe.model';

    export interface FeatureState {
        recipes: State;
    }
    export interface State {
        recipes: Recipe[];
    }
    const initialState: State = {
        recipes: [
            new Recipe(
                'Tasty Schnitzel',
                'A super-tasty Schnitzel - just awesome!',
                'https://upload.wikimedia.org/wikipedia/commons/7/72/Schnitzel.JPG',
                [
                    new Ingredient('Meat', 1),
                    new Ingredient('French Fries', 20)
                ]),
            new Recipe('Big Fat Burger',
                'What else you need to say?',
                'https://upload.wikimedia.org/wikipedia/commons/b/be/Burger_King_Angus_Bacon_%26_Cheese_Steak_Burger.jpg',
                [
                    new Ingredient('Buns', 2),
                    new Ingredient('Meat', 1)
                ])
        ]
    };

    export function recipeReducer(state = initialState, action) {
        return state;
    }
        
in recipes.module add import statements for the Store module and the recipeReducer
in the module's import property add the StoreModule with the call forFeature
AIUI the forFeature method will put the contents of the FeatureState interface into the store object where the State implementation can be assigned later
    ...
    import { StoreModule} from '@ngrx/store';

    ...
    import { recipeReducer} from './store/recipes.reducers';

    @NgModule({
        declarations: [
            ...
        ],
        imports: [
            ...
            StoreModule.forFeature('recipes', recipeReducer)
        ],
    })
    export class RecipesModule { }
        

Top

Index

n4jvp.com