Advanced State Management using ngrx (v4+)

slides.com/gerardsans |  @gerardsans

Laptops charged and ready by 14:00 ~ish

Instructions bit.ly/ng-jp-ngrx

Redux

Angular Data Layer

DATA CLIENTS

GraphQL

Firebase

ngrx

Redux

STATE MANAGEMENT

Dan Abramov

@gaearon

Main Principles

  • Unidirectional data flow
  • Single Immutable State
  • New states are created without side-effects

Unidirectional data flow

source: blog

1

2

3

4

5

Single Immutable State

  • 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)

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

Angular Data Layer

DATA CLIENTS

GraphQL

Firebase

ngrx

Redux

STATE MANAGEMENT

Rob Wormald

@robwormald

  • Re-implementation of Redux on top Angular and RxJS 5
  • ngrx suite: store, effects, router, db
  • Asynchronous Actions Middleware
  • Executes side-effects
  • Action => Effect => Action (ok/error)

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.provideStore(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 { combineReducers } from '@ngrx/store';

const combinedReducer = combineReducers({
  todos: todos,
  currentFilter: currentFilter
});

export rootReducer = (state, action) => combinedReducer(state, action);

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: [ 
    EffectsModule.run(todoEffects),
    StoreModule.provideStore(rootReducer),
  ]
})
export class AppModule {}

Effects Service

import { Actions, Effect, toPayload, 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)
    .map(toPayload)
    .switchMap(text => this.http.post('/todo', text)
      .map(response => ({ 
          type: 'ADD_TODO_SUCCESS', 
          payload: text   
        }))
    )
}

Add new todo async

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

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

Error Handling

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

Redux Dev Tools

Features

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

Why use Redux?

Main Benefits

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

Thanks!

Advanced State Management using ngrx (v4+)

By Gerard Sans

Advanced State Management using ngrx (v4+)

In this workshop we are going to use the latest version of Angular to learn advanced state management using ngrx/store and ngrx/effects. ngrx uses an implementation inspired by Redux using Observables from RxJS 5.

  • 3,043