Unidirectional dataflow in NG2 with ngrx/store
Agenda
- Problems with state management
- What is ngrx/store
- What is ngrx/effects
- Short demo
Application state
Where?
Share
Application state
- Generally passed around between multiple components/services
- Everyone modifies it and passes it around
- Really hard to keep track of
- RxJS helps, but is not enough
Solution: ngrx/store
- inspired by redux
- centralized, immutable state
- makes app state predictable
- easy to test
- time travel (logs)
- router has state
- promotes stateless components
Store
The store is an observable "DB" to which components can subscribe for data.
The logic of the application is expressed as a function mapping an observable of actions into an observable of application states.
function stateFn(
initState: AppState,
actions: Observable<action>): Observable<AppState> { … }
Application and View boundry
The application and view logic are completely separated. The dispatcher and state objects are the boundary through which the application and the view communicate. The view emits actions using the dispatcher and listens to changes in the state.
Reducers
- Pure function
- Takes in previous state and an action
- Returns the newly computed state
export interface Reducer<T> {
(state: T, action: Action): T;
}
export const counter: Reducer<number> = (state: number = 0, action: Action) => {
switch(action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
Actions
- Used to trigger a change in the store
- Trigger the reducers to recalculate state
Recap
- store is immutable
- reducers are pure functions that compute new state
- how do we handle side effects?
Solution: ngrx/effects
- Handle all async logic like AJAX calls
- Components only talk to the store
- Similar to a "stock" angular service
Effect example
import {Effect, Actions, toPayload} from "@ngrx/effects";
import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
@Injectable()
export class MainEffects {
constructor(private action$: Actions) { }
@Effect() update$ = this.action$
.ofType('SUPER_SIMPLE_EFFECT')
.switchMap( () =>
Observable.of({type: "SUPER_SIMPLE_EFFECT_HAS_FINISHED"})
);
}
Putting it all together
Writing our first reducer
// counter.ts
import { ActionReducer, Action } from '@ngrx/store';
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export function counterReducer(state: number = 0, action: Action) {
switch (action.type) {
case INCREMENT_SUCCESS:
return state + 1;
case DECREMENT_SUCCESS:
return state - 1;
case RESET:
return 0;
default:
return state;
}
}
Persisting the state
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class CounterEffects {
constructor(
private http: Http,
private actions$: Actions
) { }
@Effect() increment$ = this.actions$
// Listen for the 'LOGIN' action
.ofType('INCREMENT')
// Map the payload into JSON to use as the request body
.map(action => JSON.stringify(action.payload))
.switchMap(payload => this.http.post('/counter')
// If successful, dispatch success action with result
.map(res => ({ type: 'INCREMENT_SUCCESS' }))
// If request fails, dispatch failed action
.catch(() => Observable.of({ type: 'INCREMENT_FAILED' }))
);
}
Providing the store to our module
import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { counterReducer } from './counter';
import { CounterEffects } from './effects';
@NgModule({
imports: [
BrowserModule,
StoreModule.provideStore({
counter: counterReducer
}),
EffectsModule.run(CounterEffects),
],
})
export class AppModule {}
Using the store
import { Store } from '@ngrx/store';
import { INCREMENT, DECREMENT, RESET } from './counter';
interface AppState {
counter: number;
}
@Component({
selector: 'my-app',
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter | async }}</div>
<button (click)="decrement()">Decrement</button><button (click)="reset()">Reset Counter</button>
`
})
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>) { this.counter = store.select('counter'); }
increment() { this.store.dispatch({ type: INCREMENT }); }
decrement() { this.store.dispatch({ type: DECREMENT }); }
reset() { this.store.dispatch({ type: RESET }); }
}
THANK YOU!
Questions?
Unidirectional dataflow in NG2 with ngrx/store
By Alex Bularcă
Unidirectional dataflow in NG2 with ngrx/store
- 313