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