Advanced State

Management using new ngrx v5

by Gerard Sans |  @gerardsans

Google Developer Expert

Google Developer Expert

International Speaker

Spoken at 70 events in 23 countries

Blogger

Blogger

Community Leader

900

1.4K

Trainer

Master of Ceremonies

Master of Ceremonies

Angular Academy

Developer

Experience

extension.remotedev.io

Features

  • Save/Restore State
  • Live Debugging
  • Time travel
  • Dispatch Actions

Redux

State Management

ngrx

Redux

Dan Abramov

@gaearon

Scattered State

Redux Principles

  • Unidirectional data flow
  • Single Store
  • No side-effects

Unidirectional data flow

source: blog

1

2

3

4

5

Single State

Immutable

  • Helps tracking changes by reference
  • Improved Performance
  • Enforce by convention or using  a library. Eg: Immutable.js

Immutable by Convention

  • New array using Array Methods
    • map, filter, slice, concat
    • Spread operator (ES6) [...arr]
  • New object using Object.assign (ES6)

Example by Convention

let selectedUsers = [1, 2, 3];  
let user = { id: 4, username: 'Spiderman'};

let newSelection = selectedUsers.concat([4, 5, 6]); // [1, 2, 3, 4, 5, 6];  
let newUser = Object.assign({}, user, { admin: true });
console.log(newUser.admin) // true

// NOT VALID
selectedUsers.push(4, 5, 6);   // reference to 'selectedUsers' still the same
user.admin = true;             // reference to 'user' still the same

Immutable by using a library

  • Recommended for large teams
  • Library enforces right usage
  • Peace of mind

Using Immutable.js

let selectedUsers = Immutable.List([1, 2, 3]);  
let user = Immutable.Map({ id: 4, username: 'Spiderman'}):

let newSelection = selectedUsers.push(4, 5, 6); // [1, 2, 3, 4, 5, 6];  
let newUser = user.set('admin', true);  
newUser.get('admin') // true

Reducers

  • Reducers create new states in response to Actions applied to the current State
  • Reducers are pure functions
  • Don't produce side-effects
  • Composable

Example: pure function

// function foo(x) { return x+1; } 
let foo = x => x+1;

// pure function
foo(1); // 2
foo(1); // 2

Example: side-effect

let flag = false;
let foo = x => { 
  flag = !flag;  // side effect
  return flag ? x+1: 0;
}

// not pure function
foo(1); // 2
foo(1); // 0

Middlewares

  • Sit between Actions and Reducers
  • Used for logging, storage and asynchronous operations
  • Composable

Performance

Change Detection

source: blog

Change Detection

source: blog

ngrx

v5

What's new in v5?

  • @ngrx/schematics (@angular-devkit)
  • 'Pipeable' select operator (lettable)
  • Support custom createSelector
  • RxJS 5.5
  • UpsertOne/Many
  • Suite including
    • @ngrx/store*
    • @ngrx/effects
    • @ngrx/router-store
    • @ngrx/store-devtools*
    • @ngrx/entity*
    • @ngrx/schematics*
  • Re-implementation of Redux on top Angular and RxJS 5.
  • Colocation Support
    • StoreModule.forRoot()
    • StoreModule.forFeature()
  • Payload Type checking
  • Asynchronous Actions Middleware
  • Executes side-effects
  • Colocation Support
    • EffectsModule.forRoot()
    • EffectsModule.forFeature()

Asynchronous Actions

source: blog

1

2

3

4

5

A

A

Solution Architecture

Components Tree

source: blog

Components Tree

<app>
  <add-todo>
    <input><button>Add todo</button>
  </add-todo>
  <todo-list>
    <ul>
      <todo id="0" completed="false"><li>buy milk</li></todo>
    </ul>
  </todo-list>
  <filters>
    Show: <filter-link><a>All</a><filter-link> ... 
  </filters>
</app>

Setup

import { App } from './app';
import { StoreModule } from "@ngrx/store";
import { rootReducer } from './rootReducer';

@NgModule({
  imports: [ 
    BrowserModule, 
    StoreModule.forRoot(rootReducer)
  ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

Adding a new Todo

  1. Component subscribes
  2. Component dispatches ADD_TODO action
  3. Store executes rootReducer
  4. Store notifies Component
  5. View updates

1

2

3

4

5

Subscribing to the Store

@Component({
  template: 
      `<todo *ngFor="let todo of todos | async">{{todo.text}}</todo>`
})

export class App implements OnDestroy {
  public todos: Observable<Todo>;
  
  constructor(
    private _store: Store<TodosState>
  ) {
    this.todos = _store.let(
      state$ => state$.select(s => s.todos); 
    );
  }
}

ADD_TODO Action

// add new todo
{
  type: ADD_TODO,
  id: 1,
  text: "learn redux",
  completed: false
}

todos Reducer

const initialState = [];

const todos = (state = initialState, action:Action) => {
  switch (action.type) {
    case TodoActions.ADD_TODO: 
      return state.concat({ 
          id: action.id,
          text: action.text,
          completed: action.completed });
    default: return state;
  }
}

// {
//  todos: [], <-- todos reducer will mutate this key
//  currentFilter: 'SHOW_ALL'
// }

currentFilter Reducer

const currentFilter = (state = 'SHOW_ALL', action: Action) => {
  switch (action.type) {
    case TodoActions.SET_CURRENT_FILTER:
      return action.filter
    default: return state;
  }
}

// {
//  todos: [],
//  currentFilter: 'SHOW_ALL' <-- filter reducer will mutate this key
// }

rootReducer

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

interface Todo { 
  id: number,
  text: string,
  completed: boolean
}

export interface TodosState {
  todos: Array<Todo>;
  currentFilter: string;
}

export const rootReducer: ActionReducerMap<TodosState> = {  
  todos: todos,
  currentFilter: currentFilter
};

New State

{
  todos: [{
    id: 1,
    text: "learn redux",
    completed: false
  }],
  currentFilter: 'SHOW_ALL'
}

// {
//  todos: [], <-- we start with no todos
//  currentFilter: 'SHOW_ALL'
// }

ngrx/effects

Setup

import { EffectsModule } from '@ngrx/effects';
import { todoEffects } from './todoEffects';

@NgModule({
  imports: [ 
    StoreModule.forRoot(rootReducer),
    EffectsModule.forRoot([todoEffects]),
  ]
})
export class AppModule {}

Effects Service

import { Actions, Effect, ofType } from '@ngrx/effects';
import { TodoActions, ADD_TODO_EFFECT } from './todoActions';

@Injectable()
export class todoEffects {
  constructor(
    private actions$ : Actions,
    private actions : TodoActions ) { }

  @Effect() 
  addTodoAsync$ = this.actions$
    .ofType(ADD_TODO_EFFECT)
    .mergeMap(action => this.http.post('/todo', action.text)
      .map(response => ({ 
          type: 'ADD_TODO_SUCCESS', 
          text: text   
        }))
    )
}

Add new todo async

// Dispatch 
private addTodoAsync(input) {
  this._store.dispatch({
    type: ADD_TODO_EFFECT
    text: input.value
  });
  input.value = '';
} 

// Middleware
@Effect() 
addTodoAsync$ = this.actions$
  .ofType(ADD_TODO_EFFECT)
  .mergeMap(action => this.http.post('/todo', action.text)
    .map(response => ({ 
        type: 'ADD_TODO_SUCCESS', 
        text: text   
      }))
  )

Error Handling

@Injectable()
export class todoEffects {
  @Effect() 
  addTodoAsync$ = this.actions$
    .ofType(ADD_TODO_EFFECT)
    .mergeMap(action => this.http.post('/todo', action.text)
      .map(response => ({ 
          type: 'ADD_TODO_SUCCESS', 
          text: text   
        }))
      .catch(error => Observable.of({ 
          type: 'ADD_TODO_FAILED', 
          error: { message: error }  
        })
      )
    )
}

ngrx/entity

  • Library to manage Lists
  • High performance
    • EntityState<T>
    • Ids lookup and Entities Map
  • Reducer Helper (EntityAdapter)
    • getInitialState
    • Add/update/remove: All, One, Many
    • getSelectors: Ids, Entities, All, Total
  • Scaffolding templates for ngrx
  • Provides commands for
    • Setting initial Store and Effects
    • Actions, Reducers, Entities, Features
    • Containers + Store injected
  • Automates creation + registration

ngrx/schematics commands

> ng new ngrx-entity-todo

> npm install @ngrx/schematics --save-dev
> npm install @ngrx/{store, effects, entity, store-devtools} --save
 

> ng generate store State --root --module app.module.ts --collection @ngrx/schematics
> ng generate effect App --root --module app.module.ts --collection @ngrx/schematics


> ng generate entity Todo -m app.module.ts --collection @ngrx/schematics

Filter Actions

// src/app/reducers/currentFilter/currentFilter.actions.ts
import { Action } from '@ngrx/store';

export enum CurrentFilterActionTypes {
  SetCurrentFilter = '[Filter] Set current filter'
}

export class SetCurrentFilter implements Action {
  readonly type = CurrentFilterActionTypes.SetCurrentFilter;

  constructor(public payload: { filter: string }) {}
}

export type CurrentFilterActions = SetCurrentFilter;

Filter Reducer

// src/app/reducers/currentFilter/currentFilter.reducer.ts
export const initialState: string = 'SHOW_ALL';

export function reducer(
  state = initialState, 
  action: CurrentFilterActions): string 
{
  switch (action.type) {
    case CurrentFilterActionTypes.SetCurrentFilter:
      return action.payload.filter
    default:
      return state;
  }
}

Todos Actions

// src/app/reducers/todo/todo.reducer.ts
export enum TodoActionTypes {
  AddTodo = '[Todo] Add Todo', UpdateTodo = '[Todo] Update Todo', ...
}

let currentId = 1;
export class AddTodo implements Action {
  readonly type = TodoActionTypes.AddTodo;
  constructor(public payload: { todo: Todo }) {
    payload.todo.id = currentId++;
  }
}

export type TodoActions = LoadTodos | AddTodo | UpsertTodo | AddTodos 
| UpsertTodos | UpdateTodo | UpdateTodos | DeleteTodo | DeleteTodos | ClearTodos;

Todos Model

// src/app/reducers/todo/todo.model.ts
import { EntityState } from '@ngrx/entity';

export interface Todo {
  id: number;
  completed: boolean;
  text: string;  
}

export interface Todos {
  todos: EntityState<Todo>;
}

Composing Reducers

import * as todos from './todo/todo.reducer';
import * as currentFilter from './currentFilter/currentFilter.reducer';

export interface Filter { 
  currentFilter: string;
}
export interface Todos {
  todos: EntityState<Todo>;
}
export interface TodosState extends Todos, Filter { }

export const reducers: ActionReducerMap<TodosState> = {
  todos: todos.reducer,
  currentFilter: currentFilter.reducer
};

New Pipeable Selectors

// ngrx v4
// export const getTodos = state$ => state$.select(s => s.todos); 
// export const getCurrentFilter = state$ => state$.select('currentFilter'); 

export const getTodos = state$ => state$.pipe(
  select('todos'), 
  map(todoEntity.selectAll) 
);

export const getCurrentFilter = state$ => state$.pipe(
  select('currentFilter')
);

AppModule Setup

import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
import { reducers, metaReducers } from './reducers';

@NgModule({
  imports: [ 
    BrowserModule, 
    StoreModule.forRoot(reducers, { metaReducers }),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Why use Redux?

Main Benefits

  • Simplified Development
  • Avoids complex dependencies
  • Great Performance
  • Developer Experience

More

@MikeRyanDev

@robwormald

@brandontroberts

Rob Wormald

Mike Ryan

Brandon Roberts

@toddmotto

Todd Motto

Advanced State Management using new ngrx v5

By Gerard Sans

Advanced State Management using new ngrx v5

In this talk we are going to use the latest version of Angular and ngrx (v5) to learn advanced state management. We will introduce Redux principles before moving into ngrx/store, ngrx/effects for asynchronous actions, ngrx/entity and ngrx/schematics. ngrx/store uses an implementation inspired by Redux but using Observables from RxJS 5.5. We will also cover best practices and how to implement error handling.

  • 2,298
Loading comments...

More from Gerard Sans