Angular Workshop

RxJS and Angular with ngrx store

by Gion Kunz

Gion Kunz

  • Angular GDE since 2018
  • Front-end developer, web standards advocate
  • Specialized in spa development using Angular
  • Onsite ramp-ups, coaching and implementation support in projects
  • Author of the book "Mastering Angular Components"
  • Frequent speaker at conferences and meetups
  • Teacher for web development and UX engineering at the SAE Institute in Zurich
  • Tutor at O'Reilly Safari Online Training
  • Contributor on the Angular project, author of Chartist.js library

Founder, UI Developer, UX Enthusiast at syncrea GmbH

Topics

  • Asynchronous and Reactive Programming with RxJS
    • Basic concept
    • Basic and some advanced operators
    • Angular and RxJS
  • ngrx store
    • Centralized State Management
    • ngrx Basics
    • Build your own
    • State Management Patterns
    • Todo App Refactoring

Asynchronous & Reactive Programming

Reactive Programming with RxJS

Observable Streams

Item

Error

Completed

Simple Example

import {from} from 'rxjs';
import {filter, map} from 'rxjs/operators';

from([1, 2, 3])
  .pipe(
    map(num => num * num),
    filter(num => num > 3)
  )
  .subscribe(item => console.log(item));
  • Items are emitted through a stream of items

  • Each item flows through the stream individually

  • The subscription will cause the stream start flowing

Chaining Streams with switchMap

import {from, timer} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

from([1, 2, 3])
  .pipe(
    switchMap(num => timer(1000).pipe(map(() => num * num)))
  )
  .subscribe(item => console.log(item));

Let's Hack!

RxJS and Angular

RxJS is an integral part of Angular

  • Route Parameters

  • HttpClient Service

  • Reactive Forms

  • Component Output (EventEmitter)

Using Observables in Components

@Component({
  selector: 'my-component',
  template: '{{data}}'
})
export class MyComponent {
  asyncSubscription: Subscription;
  data: number;

  constructor() {
    this.asyncSubscription = of(1)
      .pipe(delay(2000))
      .subscribe(data => this.data = data);
  }

  ngOnDestroy() {
    this.asyncSubscription.unsubscribe();
  }
}

  • Subscriptions should be cleaned up using unsubscribe()

  • Within components, the ngOnDestroy lifecycle hook is a good spot for this task

  • Feels cumbersome and inconvenient

Subscribe in the component view

@Component({
  selector: 'my-component',
  template: '{{asyncData | async}}'
})
export class MyComponent {
  asyncData: Observable<number>;

  constructor() {
    this.asyncData = of(1)
      .pipe(delay(2000));
  }
}

  • The async Pipe is creating the subscription in the view

  • Always the latest item in the stream will be rendered

  • When the component view is destroyed, the subscriptions is automatically unsubscribed

Centralized State management

State is your Enemy

Common Issues with State

  • Synchronization
  • Distribution
  • Dependencies
  • Transitions
  • Mutability
  • Nondeterminism

Anatomy of State

Why do we even need client state?

Client

Server

update todo

update todo

reload data

show updates

Delayed Feedback

User Experience Issues

  • Bad perceived performance
  • Glitches in interactions

Project

Server

Todos

fetch todos

create todo

Out of Sync

Distribution and Synchronization Issues

  • When multiple components rely on same server state
  • Needs additional synchronization

Client Persistence State

  • We can use RxJS BehaviourSubject to cache and publish to multiple subscribers
  • For optimistic updates, we can publish a temporary update, until the server responds
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class TodoService {
  todos = new BehaviorSubject<Todos[]>([]);

  constructor(private http: HttpClient) {
    this.loadTodos();
  }

  loadTodos() {
    this.http
      .get<Todos[]>('http://localhost/api/todos')
      .subscribe(todos => this.todos.next(todos));
  }
}
addTodo(title: string) {
  this.todos.next([{
    title,
    done: false,
    order: 1
  }, ...this.todos.getValue()]);
    
  this.http.post('http://localhost/api/todos', {
    title,
    done: false,
    order: 1
  })
  .subscribe((todos) => this.todos.next(todos));
}

Optimistic Update

Server Sync

Different Types of State

client

server

persistent

persistent

transient

transient

synchronization

database, storage ...

optimistic updates, performance ...

session data ...

interaction state, UI state, URL state ...

The Persistency Gradient

Highly Persistent

Highly Transient

Database

Storage

UI State

URL State

Persistence State

Session

Cookies

Local storage

Interaction State

Determining Persistence Level

  • Lifespan requirements
  • User Experience

Centralized Reactive State

Flux Application Architecture

Action

Dispatcher

Store

View

Flux is a concept. There are many implementations:

  • Redux
  • Reflux
  • Fluxxor
  • Flummox
  • Alt

Elm

2012

Flux

2014

Redux

2015

ngrx store

2016

Influences for ngrx store 

RxJS

2010

ngrx store architecture

Action

Reducer

Store

View

1

Dispatch action

2

Current state and action

3

New state

4

Update View

Actions

export class AddTodoAction implements Action {
  readonly type = 'AddTodoAction';
  constructor(public readonly todo: ServerTodoItem) {}
}

export class UpdateTodoAction implements Action {
  readonly type = 'UpdateTodoAction';
  constructor(public readonly todoId: number,
              public readonly data: any) {}
}

Dispatching Action to Store

store.dispatch(
  new AddTodoAction({
    title: 'Todo nr 1',
    done: false,
    order: 1
  })
);

The Reducer Function

export interface Reducer<S, A extends Action> {
  (state: S, action: A): S
}
export interface CounterState {
  count: number;
}
const initialState: CounterState = {count: 0};

export function reducer(state: CounterState = initialState, 
                        action: Action): CounterState {
  switch (action.type) {
    case 'IncrementCountAction':
      return {
        ...state,
        count: state.count + 1
      };
    default: return state;
  }
}

Selecting State from Store

const count: Observable<number> = 
  store.select(state => state.count);

Let's implement our own ngrx store with 25 lines of code...

export interface CounterState {
  count: number;
}
const initialState: CounterState = {count: 0};

export function reducer(state: CounterState = initialState, 
                        action: Action): CounterState {
  switch (action.type) {
    case 'IncrementCountAction':
      return {
        ...state,
        count: state.count + 1
      };
    default: return state;
  }
}

export const store = createStore(reducer, initialState);
import { Component } from '@angular/core';
import { store } from '../counter.state';

@Component({
  selector: 'app',
  template: `
    <div class="count">{{count | async}}</div>
    <button (click)="increment()">
      Increment
    </button>
  `
})
export class CounterComponent {
  count: Observable<number> = 
    store.select(state => state.count);

  increment() {
    store.dispatch(new IncrementCounterAction());
  }
}

Flux is Balm for the Soul of your Application!

  • Centralized state eliminates dependencies and distribution issues
  • Unidirectional flow of data is highly deterministic
  • Reducers are pure functions and state transitions can be reproduced at any time
  • It is very simple to reason about your application

Let's add the famous Redux state replay with only 7 lines of code...

What about asynchronous actions? 

export class TodoService {
  loadTodos() {
    this.http
      .get<Todos[]>('http://localhost/api/todos')
      .subscribe(todos => 
        this.store.dispatch(new SetTodosAction(todos))
      );
  }
}

We can simply dispatch when async operations have completed

Using ngrx effects for managed asynchronous side effects

ngrx effects architecture

Action

Reducer

Effect

1

Dispatch action

Action

2

Map to new action

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

@Injectable()
export class TodoEffects {
  @Effect() loadTodos = this.actions.pipe(
    ofType('LoadTodosAction'),
    mergeMap(action =>
      this.http.get('http://localhost/api/todos')
        .pipe(
          map(todos => new SetTodosAction(todos))
        )
    )
  );

  constructor(private http: HttpClient, private actions: Actions) {}
}
@Component({
  selector: 'app-todos',
  template: `
    <h2>Todos</h2>
    <ul class="todo-list">
      <li *ngFor="let todo of todos | async">
        {{todo.title}}
      </li>
    </ul>`
})
export class TodosComponent {
  todos: Observable<Todo[]>;

  constructor() {
    this.todos = store
      .select(state => state.todos);

    store.dispatch(new LoadTodosAction());
  }
}

Common state Patterns

Asynchronous Operations with Error Handling

Effect

Reducer

Component

LoadItemsAction

LoadSuccessAction

LoadFailedAction

export interface State {
  loading: boolean;
  errors: string[];
  items: Item[];
}
@Effect() loadItems = this.actions.pipe(
  ofType('LoadItemsAction'),
  mergeMap(action =>
    this.http.get('http://localhost/api/items')
      .pipe(
        map(items => new LoadSuccessAction(items)),
        catchError(() => of(new LoadFailedAction('failed!'))
      )
  )
);
case 'LoadItemsAction': 
  return {
    ...state,
    loading: true
  };

case 'LoadSuccessAction':
  return {
    ...state,
    loading: false,
    items: action.items
  };

case 'LoadFailedAction':
  return {
    ...state,
    loading: false,
    errors: [action.error, ...state.errors]
  };

Optimistic Update

Effect

Reducer

Component

OptimisticUpdateAction

ServerUpdateAction

export interface State {
  updating: boolean;
  item: Item;
}
@Effect() updateItem = this.actions.pipe(
  ofType('OptimisticUpdateAction'),
  mergeMap(action =>
    this.http.post(`/api/items/${action.item.id}`, action.data)
      .pipe(
        map(item => new ServerUpdateAction(item))
      )
  )
);
case 'OptimisticUpdateAction': 
  return {
    ...state,
    updating: true,
    item: {
      ...state.item,
      ...action.optimisticData
    }
  };

case 'ServerUpdateAction':
  return {
    ...state,
    updating: false,
    item: action.item
  };

Load more button / infinite scrolling

Effect

Reducer

Component

LoadMoreAction

AppendMoreItemsAction

export interface State {
  items: Item[];
}
@Effect() loadMoreItems = this.actions.pipe(
  ofType('LoadMoreAction'),
  combineLatest(this.state.select(state => state.items.length)),
  mergeMap(([action, start]) =>
    this.http.get(`/api/items/?start=${start}&limit=10`)
      .pipe(
        map(items => new AppendMoreItemsAction(items))
      )
  )
);
case 'AppendMoreItemsAction':
  return {
    ...state,
    items: [...state.items, action.items]
  };

Process Steps / State Machine

Reducer

Component

NextStepAction

NextStepAction

export interface EnterName {
  readonly kind: 'EnterName';
  readonly name: string;
}

export interface EnterEmail {
  readonly kind: 'EnterEmail';
  readonly email: string;
}

export interface Done {
  readonly kind: 'Done';
}

export type Step = EnterName | EnterEmail | Done;

export interface State {
  readonly step: Step;
  readonly data: {name: string; email: string};
}
case 'NextStepAction':
  switch (action.step.kind) {
    case 'EnterName':
      return {
        ...state,
        data: {name: action.name},
        step: {
          kind: 'EnterEmail',
          email: ''
        }
      };
    case 'EnterEmail':
      return {
        ...state,
        data: {...state.data, email: action.email},
        step: {kind: 'Done'}
      };
  }

Conclusion

  • The Flux architecture is so simple, but solves so many issues
  • Scales linearly with increasing size and correctness of your application
  • ngrx store is reactive and combines centralized state with the power of observable streams

Let's build our own Ngrx Libary!

Summary

  • Can be built easily from scratch
  • Simple but very powerful design
  • Large portion of the code is our own (reducers, actions, state)

Todo App code refactoring

Let's implement state management with ngrx store.

Workshop Recap

  • Asynchronous and Reactive Programming with RxJS
    • Basic concept
    • Basic and some advanced operators
    • Angular and RxJS
  • ngrx store
    • Centralized State Management
    • ngrx Basics
    • Build your own
    • State Management Patterns
    • Todo App Refactoring

Let's create together.

Thanks!