NG2 & NgRx/Store:
Oren Farhi @Orizens
hi!
I AM Oren Farhi
Senior Javascript Engineer
Freelance Javascript Consultant & Tutor
You can find me at:
orizens.com
github.com/orizens
@orizens
There’s Gotta Be Something Better
Redux for Angular 2 with NgRx/Store
- Problems of State Management
- What is NgRx/Store
- Usecase App - Echoes Player
NgRx/Store In Echoes Player Agenda
Problems With
State Management
The State Of The App?
◦ Where?
◦ Share
Current State Of The App
UI ⇔ Model
Solution: ngrx/store (benefits)
1. App State is Predictable
2. Separate Logics From UI
3. Optimization in Performance
4. Easy To Test
5. Time Travel (logs)
6. Route & State
7. Promotes Stateless Components
Ngrx/Store
State Management Made Easy
Example App: Echoes Player - http://echotu.be
Store
Mother Of All States
Store is an observable “DB”
Store
{ Data }
{ Object }
[ Array ]
number
string
{ Data }
{ Data }
{ Data }
Store - json object...
bootstrap(App, [
provideStore({ videos: videosReducer })
]);
Reducers - CRUD the Store with Pure Functions (Observable)
const videos = function(state = [], action) => {
switch (action.type) {
case 'ADD_VIDEO':
return state.concat( action.payload );
default:
return state;
}
}
Reducers - CRUD the Store with Pure Functions (Observable)
let sum = [1,2,3]
sum.reduce(
(result, value) => result + value,
0
);
Action - dispatch an update to State through Reducers
addVideo(media) {
this.store.dispatch({
type: ‘ADD_VIDEO’,
payload: media
});
}
Subscribe And Observe - data projection, slice store’s data
@Component({ selector: ‘youtube-player’ })
constructor (store) {
this.player$ = store.select(store => store.player);
this.player$.subscribe(player => {
this.isFullscreen = player.isFullscreen
});
}
Subscribe - data projection - listen with Observable
@Component({ selector: ‘youtube-player’ })
constructor (store) {
this.player$ = store.select(store => store.player);
this.player$.subscribe(player => {
this.isFullscreen = player.isFullscreen
});
}
Subscribe And Observe - data projection, slice store’s data
let initialPlayerState = {
mediaId: { videoId: 'NONE' },
media: {
snippet: { title: 'No Media Yet' }
},
showPlayer: true,
playerState: 0,
isFullscreen: false
}
UI Subscribe - “async” pipe - data projection to view
<section class="player">
<player-controls
[player]="player$ | async"
(action)="handleControlAction($event)"
></player-controls>
</section>
let tempObv = player$.subscribe(player => this.player = player);
tempObv.unsubscribe(); // when view is destroyed
One Way Data Flow
Component
Reducer
Store
Action
NgRx/Store in Echoes Player
Store & The Youtube Videos Component
Store In Echoes Player
export interface EchoesState {
videos: EchoesVideos;
player: YoutubePlayerState;
nowPlaylist: YoutubeMediaPlaylist;
user: UserProfile;
search: PlayerSearch;
}
Youtube Videos - SMART Component
<youtube-videos>
Youtube Videos - 2 Stateless Components (DUMB)
<youtube-videos>
<youtube-list>
<player-search>
Youtube Videos Component (SMART)
@Component({
selector: 'youtube-videos',
directives: [ PlayerSearchComponent, YoutubeList],
template: `
<player-search
[query]="search$ | async"
(change)="resetPageToken()"
(search)="search($event)"
></player-search>
<youtube-list
[list]="videos$ | async"
(play)="playSelectedVideo($event)"
(queue)="queueSelectedVideo($event)"
></youtube-list>`
})
export class YoutubeVideos implements OnInit {}
Youtube Videos Component (SMART)
@Component({
selector: 'youtube-videos',
directives: [ PlayerSearchComponent, YoutubeList],
template: `
<player-search
[query]="search$ | async"
(change)="resetPageToken()"
(search)="search($event)"
></player-search>
<youtube-list
[list]="videos$ | async"
(play)="playSelectedVideo($event)"
(queue)="queueSelectedVideo($event)"
></youtube-list>`
})
export class YoutubeVideos implements OnInit {}
Performance Boost For Components
@Component({
selector: 'youtube-list',
template: `
<youtube-media
*ngFor="let media of list"
[media]="media"
(play)="playSelectedVideo(media)"
(queue)="queueSelectedVideo(media)"
(add)="addVideo(media)">
</youtube-media>
`,
directives: [NgFor, YoutubeMedia ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class YoutubeList {
@Input() list: Array<any>;
}
Youtube Videos Component (SMART)
@Component({
selector: 'youtube-videos',
directives: [ PlayerSearchComponent, YoutubeList],
template: `
<player-search
[query]="search$ | async"
(change)="resetPageToken()"
(search)="search($event)"
></player-search>
<youtube-list
[list]="videos$ | async"
(play)="playSelectedVideo($event)"
(queue)="queueSelectedVideo($event)"
></youtube-list>`
})
export class YoutubeVideos implements OnInit {}
Youtube Videos - Connecting to Store
export class YoutubeVideos implements OnInit {
videos$: Observable<EchoesVideos>;
search$: Observable<PlayerSearch>;
constructor(
private youtubeSearch: YoutubeSearch,
private nowPlaylistService: NowPlaylistService,
private store: Store<EchoesState>,
public youtubePlayer: YoutubePlayerService) {
this.videos$ = store.select(state => state.videos);
this.search$ = store.select(state => state.search);
}
}
Youtube Videos - Connecting to Store
export class YoutubeVideos implements OnInit {
videos$: Observable<EchoesVideos>;
search$: Observable<PlayerSearch>;
constructor(
private youtubeSearch: YoutubeSearch,
private nowPlaylistService: NowPlaylistService,
private store: Store<EchoesState>,
public youtubePlayer: YoutubePlayerService) {
this.videos$ = store.select(state => state.videos);
this.search$ = store.select(state => state.search);
}
}
Updating the Store
@Component({
selector: 'youtube-videos',
directives: [ PlayerSearchComponent, YoutubeList],
template: `
<player-search
[query]="search$ | async"
(change)="resetPageToken()"
(search)="search($event)"
></player-search>
<youtube-list
[list]="videos$ | async"
(play)="playSelectedVideo($event)"
(queue)="queueSelectedVideo($event)"
></youtube-list>`
})
export class YoutubeVideos implements OnInit {}
Youtube Videos Component
Interaction with store services:
- YoutubeSearch
- YoutubePlayer
- NowPlaylist
Youtube Videos - Updating The Store
export class YoutubeVideos implements OnInit {
videos$: Observable<EchoesVideos>;
playerSearch$: Observable<PlayerSearch>;
constructor(){ ... }
search (query: string) {
if (query.length) {
this.youtubeSearch.search(query, false);
}
}
queueSelectedVideo (media: GoogleApiYouTubeSearchResource) {
return this.nowPlaylistService.queueVideo(media.id.videoId);
}
}
Youtube Videos - Updating The Store
export class YoutubeVideos implements OnInit {
videos$: Observable<EchoesVideos>;
playerSearch$: Observable<PlayerSearch>;
constructor(){ ... }
search (query: string) {
if (query.length) {
this.youtubeSearch.search(query, false);
}
}
queueSelectedVideo (media: GoogleApiYouTubeSearchResource) {
return this.nowPlaylistService.queueVideo(media.id.videoId);
}
}
Services - dispatch to Store
@Injectable()
export class NowPlaylistService {
constructor(public store: Store<EchoesState>) {
this.playlist$ = this.store.select(state => state.nowPlaylist);
}
queueVideo (mediaId: string) {
return this.youtubeVideosInfo.api
.list(mediaId).then(response => {
this.store.dispatch({
type: QUEUE, //- const imported from reducer
payload: response.items[0]
});
return response.items[0];
});
}
}
NowPlaylist Reducer
let initialState = {
videos: [],
index: '',
filter: ''
}
export const nowPlaylist = (state = initialState, action) => {
switch (action.type) {
case NowPlaylistActions.QUEUE:
return Object.assign(
);
default:
return state;
}
NowPlaylist Reducer
let initialState = {
videos: [],
index: '',
filter: ''
}
export const nowPlaylist = (state = initialState, action) => {
switch (action.type) {
case NowPlaylistActions.QUEUE:
return Object.assign({}, state, {
videos: addMedia(state.videos, action.payload)
});
default:
return state;
}
Services - dispatch to Store
@Injectable()
export class NowPlaylistService {
constructor(public store: Store<EchoesState>) {
this.playlist$ = this.store.select(state => state.nowPlaylist);
}
queueVideo (mediaId: string) {
return this.youtubeVideosInfo.api
.list(mediaId).then(response => {
this.store.dispatch({
type: QUEUE, //- const imported from reducer
payload: response.items[0]
});
return response.items[0];
});
}
}
Ngrx/effects
Side effects of Actions
Side Effects for Queue Video
@Injectable()
export class NowPlaylistEffects {
constructor(
store$,
nowPlaylistActions
youtubeVideosInfo
){}
@Effect() queueVideoReady$ = this.store$
.whenAction(NowPlaylistActions.QUEUE_LOAD_VIDEO)
.map<GoogleApiYouTubeSearchResource>(toPayload)
.switchMap(media => this.youtubeVideosInfo.fetchVideoData(media.id.
videoId)
.map(media => this.nowPlaylistActions.queueVideo(media))
.catch(() => Observable.of(this.nowPlaylistActions.queueFailed(media)))
);
}
Testing Reducers
Testing now playlist
Loading the tested objects
import {
it,
inject,
async,
describe,
expect
} from '@angular/core/testing';
import { nowPlaylist, NowPlaylistActions } from './now-playlist';
import { YoutubeMediaItemsMock } from './mocks/youtube.media.
items';
Spec - select a video in now playlist
it('should select the chosen video', () => {
const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' };
const actual = nowPlaylist(state, {
type: NowPlaylistActions.SELECT,
payload: YoutubeMediaItemsMock[0]
});
const expected = YoutubeMediaItemsMock[0];
expect(actual.index).toBe(expected.id);
});
Spec - set initial state
it('should select the chosen video', () => {
const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' };
const actual = nowPlaylist(state, {
type: NowPlaylistActions.SELECT,
payload: YoutubeMediaItemsMock[0]
});
const expected = YoutubeMediaItemsMock[0];
expect(actual.index).toBe(expected.id);
});
Spec - select a video in now playlist
it('should select the chosen video', () => {
const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' };
const actual = nowPlaylist(state, {
type: NowPlaylistActions.SELECT,
payload: YoutubeMediaItemsMock[0]
});
const expected = YoutubeMediaItemsMock[0];
expect(actual.index).toBe(expected.id);
});
Spec - select a video in now playlist
it('should select the chosen video', () => {
const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' };
const actual = nowPlaylist(state, {
type: NowPlaylistActions.SELECT,
payload: YoutubeMediaItemsMock[0]
});
const expected = YoutubeMediaItemsMock[0];
expect(actual.index).toBe(expected.id);
});
Spec - select a video in now playlist
it('should select the chosen video', () => {
const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' };
const actual = nowPlaylist(state, {
type: NowPlaylistActions.SELECT,
payload: YoutubeMediaItemsMock[0]
});
const expected = YoutubeMediaItemsMock[0];
expect(actual.index).toBe(expected.id);
});
Ngrx DevTools
Ngrx DevTools
More NgRx To Explore:
ngrx/router
ngrx/db
...
Thanks!
ANY QUESTIONS?
You can find me at
@orizens
oren@orizens.com
http://orizens.com/services
NG2 + Ngrx/Store Workshop:
Register at http://goo.gl/EJmm7q
CREDITS
◦ Presentation template by SlidesCarnival
◦ http://orizens.com/wp/topics/adding-redux-with-
ngrxstore-to-angular-2-part-1/
◦ http://orizens.com/wp/topics/adding-redux-with-
ngrxstore-to-angular2-part-2-testing-reducers/
◦ http://orizens.com/wp/topics/angular-2-ngrxstore-
the-ngmodel-in-between-use-case-from-angular-1/
◦ Comprehensive Introduction to NgRx/Store by
btroncone
◦ Reactive Angular With Ngrx/Store by Rob Warmald
◦ https://github.com/ngrx/store
◦

Angular2 & ngrx/store: Game of States

  • 1.
    NG2 & NgRx/Store: OrenFarhi @Orizens
  • 2.
    hi! I AM OrenFarhi Senior Javascript Engineer Freelance Javascript Consultant & Tutor You can find me at: orizens.com github.com/orizens @orizens
  • 3.
    There’s Gotta BeSomething Better Redux for Angular 2 with NgRx/Store
  • 4.
    - Problems ofState Management - What is NgRx/Store - Usecase App - Echoes Player NgRx/Store In Echoes Player Agenda
  • 5.
  • 6.
    The State OfThe App? ◦ Where? ◦ Share
  • 7.
    Current State OfThe App UI ⇔ Model
  • 8.
    Solution: ngrx/store (benefits) 1.App State is Predictable 2. Separate Logics From UI 3. Optimization in Performance 4. Easy To Test 5. Time Travel (logs) 6. Route & State 7. Promotes Stateless Components
  • 9.
  • 10.
    Example App: EchoesPlayer - http://echotu.be
  • 11.
  • 12.
    Store is anobservable “DB” Store { Data } { Object } [ Array ] number string { Data } { Data } { Data }
  • 13.
    Store - jsonobject... bootstrap(App, [ provideStore({ videos: videosReducer }) ]);
  • 14.
    Reducers - CRUDthe Store with Pure Functions (Observable) const videos = function(state = [], action) => { switch (action.type) { case 'ADD_VIDEO': return state.concat( action.payload ); default: return state; } }
  • 15.
    Reducers - CRUDthe Store with Pure Functions (Observable) let sum = [1,2,3] sum.reduce( (result, value) => result + value, 0 );
  • 16.
    Action - dispatchan update to State through Reducers addVideo(media) { this.store.dispatch({ type: ‘ADD_VIDEO’, payload: media }); }
  • 17.
    Subscribe And Observe- data projection, slice store’s data @Component({ selector: ‘youtube-player’ }) constructor (store) { this.player$ = store.select(store => store.player); this.player$.subscribe(player => { this.isFullscreen = player.isFullscreen }); }
  • 18.
    Subscribe - dataprojection - listen with Observable @Component({ selector: ‘youtube-player’ }) constructor (store) { this.player$ = store.select(store => store.player); this.player$.subscribe(player => { this.isFullscreen = player.isFullscreen }); }
  • 19.
    Subscribe And Observe- data projection, slice store’s data let initialPlayerState = { mediaId: { videoId: 'NONE' }, media: { snippet: { title: 'No Media Yet' } }, showPlayer: true, playerState: 0, isFullscreen: false }
  • 20.
    UI Subscribe -“async” pipe - data projection to view <section class="player"> <player-controls [player]="player$ | async" (action)="handleControlAction($event)" ></player-controls> </section> let tempObv = player$.subscribe(player => this.player = player); tempObv.unsubscribe(); // when view is destroyed
  • 21.
    One Way DataFlow Component Reducer Store Action
  • 22.
    NgRx/Store in EchoesPlayer Store & The Youtube Videos Component
  • 23.
    Store In EchoesPlayer export interface EchoesState { videos: EchoesVideos; player: YoutubePlayerState; nowPlaylist: YoutubeMediaPlaylist; user: UserProfile; search: PlayerSearch; }
  • 24.
    Youtube Videos -SMART Component <youtube-videos>
  • 25.
    Youtube Videos -2 Stateless Components (DUMB) <youtube-videos> <youtube-list> <player-search>
  • 26.
    Youtube Videos Component(SMART) @Component({ selector: 'youtube-videos', directives: [ PlayerSearchComponent, YoutubeList], template: ` <player-search [query]="search$ | async" (change)="resetPageToken()" (search)="search($event)" ></player-search> <youtube-list [list]="videos$ | async" (play)="playSelectedVideo($event)" (queue)="queueSelectedVideo($event)" ></youtube-list>` }) export class YoutubeVideos implements OnInit {}
  • 27.
    Youtube Videos Component(SMART) @Component({ selector: 'youtube-videos', directives: [ PlayerSearchComponent, YoutubeList], template: ` <player-search [query]="search$ | async" (change)="resetPageToken()" (search)="search($event)" ></player-search> <youtube-list [list]="videos$ | async" (play)="playSelectedVideo($event)" (queue)="queueSelectedVideo($event)" ></youtube-list>` }) export class YoutubeVideos implements OnInit {}
  • 28.
    Performance Boost ForComponents @Component({ selector: 'youtube-list', template: ` <youtube-media *ngFor="let media of list" [media]="media" (play)="playSelectedVideo(media)" (queue)="queueSelectedVideo(media)" (add)="addVideo(media)"> </youtube-media> `, directives: [NgFor, YoutubeMedia ], changeDetection: ChangeDetectionStrategy.OnPush }) export class YoutubeList { @Input() list: Array<any>; }
  • 29.
    Youtube Videos Component(SMART) @Component({ selector: 'youtube-videos', directives: [ PlayerSearchComponent, YoutubeList], template: ` <player-search [query]="search$ | async" (change)="resetPageToken()" (search)="search($event)" ></player-search> <youtube-list [list]="videos$ | async" (play)="playSelectedVideo($event)" (queue)="queueSelectedVideo($event)" ></youtube-list>` }) export class YoutubeVideos implements OnInit {}
  • 30.
    Youtube Videos -Connecting to Store export class YoutubeVideos implements OnInit { videos$: Observable<EchoesVideos>; search$: Observable<PlayerSearch>; constructor( private youtubeSearch: YoutubeSearch, private nowPlaylistService: NowPlaylistService, private store: Store<EchoesState>, public youtubePlayer: YoutubePlayerService) { this.videos$ = store.select(state => state.videos); this.search$ = store.select(state => state.search); } }
  • 31.
    Youtube Videos -Connecting to Store export class YoutubeVideos implements OnInit { videos$: Observable<EchoesVideos>; search$: Observable<PlayerSearch>; constructor( private youtubeSearch: YoutubeSearch, private nowPlaylistService: NowPlaylistService, private store: Store<EchoesState>, public youtubePlayer: YoutubePlayerService) { this.videos$ = store.select(state => state.videos); this.search$ = store.select(state => state.search); } }
  • 32.
    Updating the Store @Component({ selector:'youtube-videos', directives: [ PlayerSearchComponent, YoutubeList], template: ` <player-search [query]="search$ | async" (change)="resetPageToken()" (search)="search($event)" ></player-search> <youtube-list [list]="videos$ | async" (play)="playSelectedVideo($event)" (queue)="queueSelectedVideo($event)" ></youtube-list>` }) export class YoutubeVideos implements OnInit {}
  • 33.
    Youtube Videos Component Interactionwith store services: - YoutubeSearch - YoutubePlayer - NowPlaylist
  • 34.
    Youtube Videos -Updating The Store export class YoutubeVideos implements OnInit { videos$: Observable<EchoesVideos>; playerSearch$: Observable<PlayerSearch>; constructor(){ ... } search (query: string) { if (query.length) { this.youtubeSearch.search(query, false); } } queueSelectedVideo (media: GoogleApiYouTubeSearchResource) { return this.nowPlaylistService.queueVideo(media.id.videoId); } }
  • 35.
    Youtube Videos -Updating The Store export class YoutubeVideos implements OnInit { videos$: Observable<EchoesVideos>; playerSearch$: Observable<PlayerSearch>; constructor(){ ... } search (query: string) { if (query.length) { this.youtubeSearch.search(query, false); } } queueSelectedVideo (media: GoogleApiYouTubeSearchResource) { return this.nowPlaylistService.queueVideo(media.id.videoId); } }
  • 36.
    Services - dispatchto Store @Injectable() export class NowPlaylistService { constructor(public store: Store<EchoesState>) { this.playlist$ = this.store.select(state => state.nowPlaylist); } queueVideo (mediaId: string) { return this.youtubeVideosInfo.api .list(mediaId).then(response => { this.store.dispatch({ type: QUEUE, //- const imported from reducer payload: response.items[0] }); return response.items[0]; }); } }
  • 37.
    NowPlaylist Reducer let initialState= { videos: [], index: '', filter: '' } export const nowPlaylist = (state = initialState, action) => { switch (action.type) { case NowPlaylistActions.QUEUE: return Object.assign( ); default: return state; }
  • 38.
    NowPlaylist Reducer let initialState= { videos: [], index: '', filter: '' } export const nowPlaylist = (state = initialState, action) => { switch (action.type) { case NowPlaylistActions.QUEUE: return Object.assign({}, state, { videos: addMedia(state.videos, action.payload) }); default: return state; }
  • 39.
    Services - dispatchto Store @Injectable() export class NowPlaylistService { constructor(public store: Store<EchoesState>) { this.playlist$ = this.store.select(state => state.nowPlaylist); } queueVideo (mediaId: string) { return this.youtubeVideosInfo.api .list(mediaId).then(response => { this.store.dispatch({ type: QUEUE, //- const imported from reducer payload: response.items[0] }); return response.items[0]; }); } }
  • 40.
  • 41.
    Side Effects forQueue Video @Injectable() export class NowPlaylistEffects { constructor( store$, nowPlaylistActions youtubeVideosInfo ){} @Effect() queueVideoReady$ = this.store$ .whenAction(NowPlaylistActions.QUEUE_LOAD_VIDEO) .map<GoogleApiYouTubeSearchResource>(toPayload) .switchMap(media => this.youtubeVideosInfo.fetchVideoData(media.id. videoId) .map(media => this.nowPlaylistActions.queueVideo(media)) .catch(() => Observable.of(this.nowPlaylistActions.queueFailed(media))) ); }
  • 42.
  • 43.
    Loading the testedobjects import { it, inject, async, describe, expect } from '@angular/core/testing'; import { nowPlaylist, NowPlaylistActions } from './now-playlist'; import { YoutubeMediaItemsMock } from './mocks/youtube.media. items';
  • 44.
    Spec - selecta video in now playlist it('should select the chosen video', () => { const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' }; const actual = nowPlaylist(state, { type: NowPlaylistActions.SELECT, payload: YoutubeMediaItemsMock[0] }); const expected = YoutubeMediaItemsMock[0]; expect(actual.index).toBe(expected.id); });
  • 45.
    Spec - setinitial state it('should select the chosen video', () => { const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' }; const actual = nowPlaylist(state, { type: NowPlaylistActions.SELECT, payload: YoutubeMediaItemsMock[0] }); const expected = YoutubeMediaItemsMock[0]; expect(actual.index).toBe(expected.id); });
  • 46.
    Spec - selecta video in now playlist it('should select the chosen video', () => { const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' }; const actual = nowPlaylist(state, { type: NowPlaylistActions.SELECT, payload: YoutubeMediaItemsMock[0] }); const expected = YoutubeMediaItemsMock[0]; expect(actual.index).toBe(expected.id); });
  • 47.
    Spec - selecta video in now playlist it('should select the chosen video', () => { const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' }; const actual = nowPlaylist(state, { type: NowPlaylistActions.SELECT, payload: YoutubeMediaItemsMock[0] }); const expected = YoutubeMediaItemsMock[0]; expect(actual.index).toBe(expected.id); });
  • 48.
    Spec - selecta video in now playlist it('should select the chosen video', () => { const state = { index: '', videos: [...YoutubeMediaItemsMock], filter: '' }; const actual = nowPlaylist(state, { type: NowPlaylistActions.SELECT, payload: YoutubeMediaItemsMock[0] }); const expected = YoutubeMediaItemsMock[0]; expect(actual.index).toBe(expected.id); });
  • 49.
  • 50.
  • 51.
    More NgRx ToExplore: ngrx/router ngrx/db ...
  • 52.
    Thanks! ANY QUESTIONS? You canfind me at @orizens oren@orizens.com http://orizens.com/services NG2 + Ngrx/Store Workshop: Register at http://goo.gl/EJmm7q
  • 53.
    CREDITS ◦ Presentation templateby SlidesCarnival ◦ http://orizens.com/wp/topics/adding-redux-with- ngrxstore-to-angular-2-part-1/ ◦ http://orizens.com/wp/topics/adding-redux-with- ngrxstore-to-angular2-part-2-testing-reducers/ ◦ http://orizens.com/wp/topics/angular-2-ngrxstore- the-ngmodel-in-between-use-case-from-angular-1/ ◦ Comprehensive Introduction to NgRx/Store by btroncone ◦ Reactive Angular With Ngrx/Store by Rob Warmald ◦ https://github.com/ngrx/store ◦