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
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
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();
}
...
}
{shoppingList:>
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
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
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
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
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
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
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
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