Angular
the Redux way

Some thoughts on application state architecture, user interface state management and other pitfalls.

by Gion Kunz

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 create together.

Thanks!

Angular the Redux Way

By Gion Kunz

Angular the Redux Way

Some thoughts on application state architecture, user interface state management and other pitfalls.

  • 686