使用 Redux 風格開發 Angular

Redux 起源於 React 社群,算是一種 design pattern,適用於某些情境,也提供一些優點,Angular 也有 Redux 的實作,但 Angular 是否該使用 Redux 呢?

Version


macOS 10.12.4
Angular CLI 1.0.0
Angular 4.0.1
ngrx/store 2.2.1

為什麼會有 Redux?


ngrx004

Facebook 由於在界面上有多個 component 都可讀取 message,且又同時從 server 端下載 message,也就是當每個 component 的 message 被讀取後,必須更新 unread message count,但 Facebook 發現 unread message count 總是算不準,有解不完的 bug,因此提出了 Flux 架構。

Redux 靈感來自於 Flux 架構,在 Angular 目前有兩套實作,一套是 Ng-redux,另一套是 Ngrx

  • Ng-redux 核心仍使用 Redux,增加對 Angular 的支援。
  • Ngrx 只有概念使用 Redux,核心完全使用 RxJS 重新實作。

目前 Ngrx 在 GitHub 的星星數遠高於 Ng-redux,本文將以 Ngrx 討論。

Flux 是一種概念,Redux 是 Flux 在 React 的實作,Ngrx 則是 Redux 在 Angular 的實作。

Ngrx 簡介


ngrx005

將 component 與 service 的資料統一放到 store,當 store 的資料有更新,將會自動更新到有 subscribe 的 component 與 service。1 1本圖片來自於 Brain Troncone, A Comprehensive Introduction to @ngrx/store

ngrx006

在使用 Ngrx 之前,首先必須了解一些專有名詞。2 2本圖片來自於 Building a Redux application with Anguar 2 - Part 1

View

相當於 component,主要在顯示使用者介面。

Action

當 component 有任何 event 時,會對 Ngrx 發出 action。

Middleware

負責存取對 server 端的 API,本文暫不討論此部分。

Dispatcher

負責接受 component 傳來的 action,並將 action 傳給 reducer。

Store

可是為 Ngrx 在瀏覽器端的資料庫,各 component 的資料都可統一放在這裡。

Reducer

根據 dispatcher 傳來的 action,決定該如何寫入 state。

當 state 有改變時,將通知有 subscribe 該 state 的 component 自動更新。

State

存放在 store 內的資料。

ngrx007

有些東西 Ngrx 已經幫我們做了,真的要我們自己實作只有 4 個部份,且資料流為單向的 : Component -> Action -> Reducer -> Store -> Component。

安裝 Ngrx


Installation

1
~/MyProject $ npm install @ngrx/core @ngrx/store --save

安裝 @ngrx/core@ngrx/store

Counter 範例


ngrx002

AppModule

app.module.ts3 3GitHub Commit : app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {StoreModule} from '@ngrx/store';
import {counterReducer} from './stores/counter/counter.reducer';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(counterReducer),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

14 行

1
2
3
4
5
6
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(counterReducer),
],

須在 AppModule import StoreModule.provideStore(),並傳入 reducer。

Component

app.component.html4 4GitHub Commit : app.component.html

1
2
3
4
<p>Counter: {{ counter | async }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>

counter 加上 async pipe,由 async 負責將 ngrx/store 來 subscribe 與 unsubscribe。

但這有個限制,counter 必須為宣告成 Observable<number> 型別。

Component 包含了 IncrementDecrementReset 3 個 button。

app.component.ts5 5GitHub Commit : app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import {Component} from '@angular/core';
import {CounterState} from './stores/counter/counter.store';
import {Observable} from 'rxjs/observable';
import {Store} from '@ngrx/store';
import {DECREMENT, INCREMENT, RESET} from './stores/counter/counter.action';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
counter: Observable<number>;

constructor(private store: Store<CounterState>) {
this.counter = store.select('counter');
}

increment() {
this.store.dispatch({
type: INCREMENT,
payload: {
value: 1
}
});
}

decrement() {
this.store.dispatch({
type: DECREMENT,
payload: {
value: 1
}
});
}

reset() {
this.store.dispatch({type: RESET});
}
}

15 行

1
2
3
constructor(private store: Store<CounterState>) {
this.counter = store.select('counter');
}

ngrx/storeStore 依賴注入,它是個泛型,需傳入自己的 state 型別。

由 store 的 select() 傳回 store 內的 counter field,此為 Observable 型別。

13 行

1
counter: Observable<number>;

宣告 counterObservable 型別,其泛型為 number

為什麼 counter 不是 number 型別,而是 Observable<number> 呢?因為 store.select() 回傳的型別為 Observable

19 行

1
2
3
4
5
6
7
8
increment() {
this.store.dispatch({
type: INCREMENT,
payload: {
value: 1
}
});
}

Increment button 的 event handler。

將 action 透過 store 的 dispatch() 傳入,action 物件包含 typepayload 兩個 field,type 為欲 dispatch 的 action,而 payload 則為欲透過 dispatch 傳入的資料,可自行決定其物件屬性,之後會由 reducer 根據 action 寫入 state。

可將 dispatch 概念上想成類似 event 的 emit。

Action

counter.action.ts6 6GitHub Commit : counter.action.ts

1
2
3
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

定義 action 常數,將來 component 可用 dispatch() 發布 action, 然後 reducer 再根據 action 做 switch 判斷寫入 state。

Reducer

counter.reducer.ts7 7GitHub Commit : counter.reducer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {CounterState, INITIAL_COUNTER_STATE} from './counter.store';
import {DECREMENT, INCREMENT, RESET} from './counter.action';
import {Action} from '@ngrx/store';

export function counterReducer(state: CounterState = INITIAL_COUNTER_STATE, action: Action): CounterState {
const {type, payload} = action;

switch (type) {
case INCREMENT:
return {...state, counter: state.counter + payload.value};

case DECREMENT:
return {...state, counter: state.counter - payload.value};

case RESET:
return INITIAL_COUNTER_STATE;

default:
return state;
}
}

一個 state 會搭配一個 reducer,由 reducer 寫入 state。

第 5 行

1
export function counterReducer(state: CounterState = INITIAL_COUNTER_STATE, action: Action): CounterState {

Reducer 會以 state 與 action 為參數,並寫入 state。

  • 第 1 個參數為 state,可設定 reducer 一開始的預設 state,傳入目前的 state。
  • 第 2 個參數為 action,傳入目前的 action。

第 6 行

1
const {type, payload} = action;

根據 ngrx 的 dispatcher.d.tsAction 的定義如下 :

1
2
3
4
export interface Action {
type: string;
payload?: any;
}

Action 的兩個 field 為 typepayload,因此我們可以使用 TypeScript 2.1 的 object destruction 將 action 分解成 typepayload 兩個變數。

第 8 行

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (type) {
case INCREMENT:
return {...state, counter: state.counter + payload.value};

case DECREMENT:
return {...state, counter: state.counter - payload.value};

case RESET:
return INITIAL_COUNTER_STATE;

default:
return state;
}

典型的 Redux 風格,在 reducer 內會根據 action 的 type 做 switch case

第 10 行

1
return {...state, counter: state.counter + payload.value};

Redux 為 FP (Functional Programming) 思維的產物,要求 reducer 必須為 pure function,因此必須回傳一個新的 state,而不是去修改原本的 state。

TypeScript 2.1 提供了 object spread,…state 會將整個物件的屬性加以展開,之後的參數為要修改的屬性值,{} 會將物件屬性加以合併,並回傳新的物件。

傳統會使用 Object.assign() 的寫法,但寫法並不直覺,且因為其第二個參數需傳入物件,還必須多一層 {},建議使用 object spread 寫法可讀性較高。

Store

counter.store.ts8 8GitHub Commit : counter.store.ts

1
2
3
4
5
6
7
export interface CounterState {
counter: number;
}

export const INITIAL_COUNTER_STATE: CounterState = {
counter: 0
};

在 store 定義自己的 state 型別。

第 1 行

1
2
3
export interface CounterState {
counter: number;
}

定義 CounterState 與其 field。

使用 interface 即可,因為 state 型別主要是給 TypeScript 編譯檢查與 IntelliSense 使用,而 JavaScript 沒有 interface,故編譯後 interface 會消失,若 state 使用 class,將來編譯後還會存在 class。

第 5 行

1
2
3
export const INITIAL_COUNTER_STATE: CounterState = {
counter: 0
};

定義 INITIAL_COUNTER_STATE 常數,為 CounterState 的初始狀態。

Todo 範例


ngrx003

AppModule

app.module.ts9 9GitHub Commit : app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {TodoListComponent} from './todo-list/todo-list.component';
import {TodoDashboardComponent} from './todo-dashboard/todo-dashboard.component';
import {StoreModule} from '@ngrx/store';
import {todoReducer} from './stores/todo/todo.reducer';

@NgModule({
declarations: [
AppComponent,
TodoListComponent,
TodoDashboardComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(todoReducer)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

18 行

1
2
3
4
5
6
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(todoReducer)
],

須在 AppModule import StoreModule.provideStore(),並傳入 reducer。

Component

AppComponent

app.component.html10 10GitHub Commit : app.component.html

1
2
3
<h1>Todo</h1>
<app-todo-list></app-todo-list>
<app-todo-dashboard></app-todo-dashboard>

包含了 TodoListTodoDashboard 兩個 component。

TodoList

todo-list.component.html11 11GitHub Commit : todo-list.component.html

1
2
3
4
5
6
7
8
<input type="text" #title>
<button (click)="addTodo(title)">Add</button>
<ul>
<li *ngFor="let todo of todos | async">
{{ todo.title }}
<button (click)="removeTodo(todo)">Remove</button>
</li>
</ul>

todosObservable,需加上 asyncngrx/store 來 subscribe 與 unsubscribe。

但這有個限制,todos 必須為宣告成 Observable<Todo[]> 型別。

Component 包含了AddRemove 2 個 button。

todo-list.component.ts12 12GitHub Commit : todo-list.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import {Component} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Todo, TodoState} from '../stores/todo/todo.store';
import {Store} from '@ngrx/store';
import {ADD_TODO, REMOVE_TODO} from '../stores/todo/todo.action';

@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html'
})
export class TodoListComponent {
todos: Observable<Todo[]>;

constructor(private store: Store<TodoState>) {
this.todos = store.select('todos');
}

addTodo(input: HTMLInputElement) {
if (!input.value) {
return;
}

this.store.dispatch({
type: ADD_TODO,
payload: {
title: input.value
}
});

input.value = '';
}

removeTodo(todo: Todo) {
this.store.dispatch({
type: REMOVE_TODO,
payload: {
id: todo.id
}
});
}
}

14 行

1
2
3
constructor(private store: Store<TodoState>) {
this.todos = store.select('todos');
}

ngrx/storeStore 依賴注入,它是個泛型,需傳入自己的 state 型別。

由 store 的 select() 傳回 store 內的 todos field,此為 Observable 型別。

12 行

1
todos: Observable<Todo[]>;

宣告 todosObservable 型別,其泛型為 Todo[]

為什麼 todos 不是 Todo[] 型別,而是 Observable<Todo[]> 呢?因為 store.select() 回傳的型別為 Observable

18 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
addTodo(input: HTMLInputElement) {
if (!input.value) {
return;
}

this.store.dispatch({
type: ADD_TODO,
payload: {
title: input.value
}
});

input.value = '';
}

Add button 的 event handler。

ADD_TODO action 透過 store 的 dispatch() 傳入,action 物件包含 typepayload 兩個 field,typeADD_TODO action,而 payload 則為欲新增資料的 title,之後會由 reducer 根據目前 action 寫入 state。

33 行

1
2
3
4
5
6
7
8
removeTodo(todo: Todo) {
this.store.dispatch({
type: REMOVE_TODO,
payload: {
id: todo.id
}
});
}

Remove button 的 event handler。

REMOVE_TODO action 透過 store 的 dispatch() 傳入,action 物件包含 typepayload 兩個 field,typeREMOVE_TODO action,而 payload 則為欲移除資料的 id,之後會由 reducer 根據目前 action 寫入 state。

TodoDashboard

todo-dashboard.component.html13 13GitHub Commit : todo-dashboard.component.html

1
2
3
4
5
6
7
8
9
<p>
Last Update: {{ lastUpdate | async | date:'mediumTime'}}
</p>
<p>
Total items: {{ (todos | async).length }}
</p>
<p>
<button (click)="clearTodos()">Clear All</button>
</p>

顯示最後更新時間與 Todo 筆數。

lastUpdatetodos 均為 Observable,需加上 asyncngrx/store 來 subscribe 與 unsubscribe。

Component 包含了 Clear All button。

todo-dashboard.component.ts14 14GitHub Commit : todo-dashboard.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Component} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Todo, TodoState} from '../stores/todo/todo.store';
import {Store} from '@ngrx/store';
import {CLEAR_TODOS} from '../stores/todo/todo.action';

@Component({
selector: 'app-todo-dashboard',
templateUrl: './todo-dashboard.component.html'
})
export class TodoDashboardComponent {
todos: Observable<Todo[]>;
lastUpdate: Observable<Date>;

constructor(private store: Store<TodoState>) {
this.todos = store.select('todos');
this.lastUpdate = store.select('lastUpdate');
}

clearTodos() {
this.store.dispatch({
type: CLEAR_TODOS
});
}
}

15 行

1
2
3
4
constructor(private store: Store<TodoState>) {
this.todos = store.select('todos');
this.lastUpdate = store.select('lastUpdate');
}

ngrx/storeStore 依賴注入,它是個泛型,需傳入自己的 state 型別。

由 store 的 select() 傳回 store 內的 todoslastUpdatefield,皆為 Observable 型別。

12 行

1
2
todos: Observable<Todo[]>;
lastUpdate: Observable<Date>;

宣告 todosObservable 型別,其泛型為 Todo[]

宣告 lastUpdateObservable 型別,其泛型為 Date

20 行

1
2
3
4
5
clearTodos() {
this.store.dispatch({
type: CLEAR_TODOS
});
}

Clear All button 的 event handler。

CLEAR_TODOS action 透過 store 的 dispatch() 傳入,action 物件包含 typepayload 兩個 field,typeREMOVE_TODO action,因為沒有要傳入的資料,因此不需 payload,之後會由 reducer 根據目前 action 寫入 state。 。

Action

todo.action.ts15 15GitHub Commit : todo.action.ts

1
2
3
4
export const ADD_TODO    = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const CLEAR_TODOS = 'CLEAR_TODOS';
export const DEFAULT = 'DEFAULT';

在 action 定義自己的 action 常數。

實務上 action 常數會以 component 或 service 所要 dispatch 的 event 設計,以本範例而言,在 component 有 AddRemoveDelete All 3 個 button,因此會配合 3 個 button 設計出 ADD_TODOREMOVE_TODOCLEAR_TODOS 3 個 action。

Reducer

todo.reducer.ts16 16GitHub Commit : todo.reducer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import {INITIAL_TODO_STATE, TodoState} from './todo.store';
import {ADD_TODO, CLEAR_TODOS, REMOVE_TODO} from './todo.action';
import {Action} from '@ngrx/store';

export function todoReducer(state: TodoState = INITIAL_TODO_STATE, action: Action): TodoState {
const {type, payload} = action;

switch (type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, {
id: state.todos.length + 1,
title: payload.title
}],
lastUpdate: new Date()
};

case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== payload.id),
lastUpdate: new Date()
};

case CLEAR_TODOS:
return {
...state,
todos: [],
lastUpdate: new Date()
};

default:
return state;
}
}

在 reducer 定義自己的寫入 state 邏輯。

第 9 行

1
2
3
4
5
6
7
8
9
case ADD_TODO:
return {
...state,
todos: [...state.todos, {
id: state.todos.length + 1,
title: payload.title
}],
lastUpdate: new Date()
};

使用 object spread 方式傳回新物件。

todos 為陣列,而 ADD_TODO 主要的目的就是將新的物件加到 todos 陣列,因此可使用 array spread 方式傳回新的陣列。

19 行

1
2
3
4
5
6
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== payload.id),
lastUpdate: new Date()
};

一樣使用 object spread 方式傳回新的物件。

REMOVE_TODO 主要為移除 payload.idTodo,但因為 Ngrx 要求為 pure function,因此改用 filter() 過濾 todo.id 不為 payload.id

26 行

1
2
3
4
5
6
case CLEAR_TODOS:
return {
...state,
todos: [],
lastUpdate: new Date()
};

一樣使用 object spread 方式傳回新的物件。

CLEAR_TODOS 為清除所有 Todo,但因為 Ngrx 要求為 pure function,因此傳回 [] 空陣列。

Store

todo.store.ts17 17GitHub Commit : todo.store.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface Todo {
id: number;
title: string;
}

export interface TodoState {
todos: Todo[];
lastUpdate: Date;
}

export const INITIAL_TODO_STATE: TodoState = {
todos: [],
lastUpdate: null
};

在 store 定義自己的 state 型別。

第 6 行

1
2
3
4
export interface TodoState {
todos: Todo[];
lastUpdate: Date;
}

定義 TodoState 與其 field。

其中 todosTodo 型別的陣列。

第 1 行

1
2
3
4
export interface Todo {
id: number;
title: string;
}

定義 Todo 型別。

11 行

1
2
3
4
export const INITIAL_TODO_STATE: TodoState = {
todos: [],
lastUpdate: null
};

定義 INITIAL_TODO_STATE 常數,為 TodoState 的初始狀態。

Ngrx DevTools


Ngrx 所提供的開發者工具,讓我們可以針對 Ngrx 的 store 與 action 做 debug。

Installation

1
~/MyProject $ npm install @ngrx/store-devtools --save

安裝 @ngrx/store-devtools

ngrx000

安裝 Redux DevTools 到 Chrome。

AppModule

app.module.ts18 18GitHub Commit : app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {TodoListComponent} from './todo-list/todo-list.component';
import {TodoDashboardComponent} from './todo-dashboard/todo-dashboard.component';
import {StoreModule} from '@ngrx/store';
import {todoReducer} from './stores/todo/todo.reducer';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';

@NgModule({
declarations: [
AppComponent,
TodoListComponent,
TodoDashboardComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(todoReducer),
StoreDevtoolsModule.instrumentOnlyWithExtension(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

19 行

1
2
3
4
5
6
7
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.provideStore(todoReducer),
StoreDevtoolsModule.instrumentOnlyWithExtension(),
],

須在 AppModule import StoreDevtoolsModule.instrumentOnlyWithExtension()

ngrx001

可在 Chrome 使用 Ngrx DevTools 對 state 與 action 做 debug,並可使用 time-traveling 的方式一個 action 一個 action 的執行。

Ngrx 的特色


  • 將所有 state 統一放在單一 store 內。
  • 所有寫入 state 的邏輯都統一放在單一 reducer 內,且必須為 pure function。
  • Component 不寫邏輯,只負責 dispatch 適當的 action。
  • 當 state 變更,component 會自動更新。

Ngrx 的優點


  • 單向的資料流程,程式碼較易理解與 debug。
  • 將 state 的變更邏輯統一寫在 reducer 內,而非分散在各 component,較容易維護。
  • 由於 reducer 為 pure function,很容易寫單元測試。
  • 資料邏輯與 framework 解耦合,action/reducer/store 獨立於 framework,將來要移植到其他 framework 很方便。
  • 有 Ngrx DevTools 方便做 time-traveling 方式的 debug。
  • 可將使用者行為錄製下來。

Ngrx 的缺點


  • 為 FP 思維產物,若只熟 OOP 較難以掌握。
  • 維護的人須事先有 Redux 觀念,否則無法維護,學習門檻較高。
  • Reducer 需寫成 pure function,難度較高。
  • 有點 over design 味道。

什麼時候該使用 Ngrx?


  • 當多個 component 需使用共用資料,且各 component 的操作會影像其他 component 的結果。
  • 資料可能同時被多個 component 修改,甚至同時被 server API 修改。
  • 需要實作 undo/redo 功能。

You’ll know when you need Flux. If you aren’t sure if you need it, you don’t need it.

套句 React How-to 的名言,當你需要 Ngrx 的時候再使用 Ngrx,若你不確定,就不要使用,避免因誤用而 over design。

Conclusion


  • Ngrx 有一點 over design,相當於 command 模式與 Observable 模式的合體。
  • Ngrx 就跟所有的 design pattern 一樣,都會使設計複雜化,並不是所有應用都適合使用 Ngrx,必須看需求用在刀口上。
  • RxJS 出來之後,Ngrx 的寫法重要性不若以往,簡單的應用直接在 service 使用 RxJS 即可。

Sample Code


完整的範例可以在我的 GitHub 上找到 CounterNgrxTodoNgrx

Reference


ngrx, ngrx/store
Mosh Hamedani, Build Enterprise Applications with Angular 2
Angular University, Angular Ngrx Crash Course Part 1: Ngrx Store - Learn It By Understanding The Original Facebook Counter Bug
Angular University, Angular Service Layers: Redux, RxJs and Ngrx Store - When to Use a Store And Why ?
Hristo Georgiev, Building a Redux application with Anguar 2 - Part 1
Hristo Georgiev, Building a Redux application with Anguar 2 - Part 2
Brain Troncone, A Comprehensive Introduction to @ngrx/store
egghead.io, Build Redux Style Application with Angular 2, RxJS, and ngrx/store
Michal Majewski, What I have learned using ngrx/Redux with Angular 2
Dan Abramov, You Might Not Need Redux
Pete Hunt, React How-to

2017-04-19