evan schultz - angular camp - ng2-redux

41
Evan Schultz Developer @Rangleio Some rights reserved - Creative Commons 2.0 by-sa BUILDING ANGULAR 2 APPLICATIONS WITH REDUX

Upload: evan-schultz

Post on 12-Jan-2017

850 views

Category:

Documents


0 download

TRANSCRIPT

Evan Schultz Developer @Rangleio

Some rights reserved - Creative Commons 2.0 by-sa

BUILDING ANGULAR 2 APPLICATIONS WITH REDUX

A Typical Complex SPA• Lots of parts.

• Everything is connected to everything else.

• Changing anything breaks something somewhere.

Best Solutions Known as of Now• Component-based UI.

• Unidirectional data-flow.

• A “stateless” approach deeper in the stack.

Stateless Architecture

Why Stateless is Good• A lot of us were raised on OOP. Objects are stateful.

• The effect of calling a method depends on the arguments and the object’s state.

• Very painful to test.

• Very hard to understand.

• Aims to mimic the real world.

• But why replicate the unnecessary complexity?

Alternative: Pure Functions• The output of a pure function depends only on inputs.

• Pure functions have no side-effects.

• Pure functions have no state.

• Much easier to understand.

• Much easier to test.

• Very easy to build up through composition.

• Widely used on the server side today.

How State Really Becomes Painful

Component Model

Model

Model

Model

Component

Component

Component

Unidirectional Data Flow with Flux

Component

Store

API, etc.

Component

Dispatcher

Action(s)ActionCreator

Can We Do Better?

Getting More Stateless with Redux• https://github.com/rackt/redux

• Key concepts: a single store + state “reducers”.

• Can a single store be a good idea?

• What the heck is a “reducer”?

Reducer Functions• Basic reduce():

function addOneValue (value, state) { return state + value;}

[1,2,3,4,5].reduce(addOneValue, 0);

• Running through the items:

1: 0 => 12: 1 => 33: 3 => 64: 6 => 105: 10 => 15

Action Reducers in Redux• Take an action and a state, return a new state.

• Your app’s state is a reduction over the actions.

• Each reducer operates on a subset of the global state.

• Simple reducers are combined into complex ones.

An Example

export const lineupReducer = (state: ILineup[] = INITIAL_STATE, action) => { switch (action.type) { case PARTY_JOINED: return [...state, action.payload]; case PARTY_LEFT: return state .filter(n => n.partyId !== action.payload.partyId); case PARTY_SEATED: return state .filter(n => n.partyId !== action.payload.partyId); default: return state; }};

Testing Is Easy

describe('the lineup reducer', () => { it('should allow parties to join the lineup', () => { const initialState = lineupReducer([]); const expectedState = [{ partyId: 1, numberOfPeople: 2 }];

const partyJoined = { type: PARTY_JOINED, payload: { partyId: 1, numberOfPeople: 2 } };

const nextState = lineupReducer(initialState, partyJoined); expect(nextState).to.deep.equal(expectedState); });});

Why Is This a Good Idea?• Reducers are pure functions – easy to understand, easy to test.

• Reducers are synchronous.

• Data logic is 100% separated from view logic.

• You can still have modularity by combining reducers.

• New opportunities for tools.

How Do We Avoid Mutating State?• Reducers are supposed to not change the old state. But how do we keep them honest?

• Immutable data structures store data without introducing “state”.

• Object.freeze() - shallow.

• “Seamless Immutable”: frozen all the way down.

• But what happens when you want to modify your data?

Derived Immutables• You can’t change immutable objects. But you need to.

• So, you derive new ones. I.e., make a new immutable that is different in a specified way, without altering the original.

• “Immutable.JS” is the library to use.var newData = data.setIn( ['foo', 'bar', 'baz'], 42);

• This is a key building block for stateless architecture.

Change Detection• Angular 2 has OnPush change detection

• Only fires when reference to an Input changes

• Do not need to do property by property checking

• If used properly, can improve the performance of your application

Angular 2 + Redux =

ng2-redux

ng2-redux• Angular 2 bindings for Redux

• https://github.com/angular-redux/ng2-redux

• npm install ng2-redux

• Store as Injectable service

• Expose store as an Observable

• Compatible with existing Redux ecosystem

• https://github.com/xgrommx/awesome-redux

Angular 2 - Register the Provider

import { bootstrap } from '@angular/platform-browser-dynamic';import { NgRedux } from 'ng2-redux';

import { RioSampleApp } from './containers/sample-app';import { ACTION_PROVIDERS } from './actions';

bootstrap(RioSampleApp, [ NgRedux, ACTION_PROVIDERS]);

Angular 2 - Configure the Store

import { combineReducers } from 'redux';import { IMenu, menuReducer } from './menu';import { ITables, tableReducer } from './tables';import { ILineup, lineupReducer } from './lineup';

export interface IAppState { lineup?: ILineup[]; menu?: IMenu; tables?: ITables;};

export default combineReducers<IAppState>({ lineup: lineupReducer, menu: menuReducer, tables: tableReducer});

Angular 2 - Configure the Store

import { NgRedux } from 'ng2-redux';import { IAppState } from '../reducers';import rootReducer from '../reducers';import { middleware, enhancers } from '../store';

@Component({ selector: 'rio-sample-app', // ... })export class RioSampleApp {

constructor(private ngRedux: NgRedux<IAppState>) { ngRedux .configureStore(rootReducer, {}, middleware, enhancers); }};

Angular 2 - Dumb Component

• Receives data from container or smart component @Input() lineup: ILineup[];

• Emits events up to the parent @Output() partyJoined: EventEmitter<any> = new EventEmitter();

• Responsible for rendering only

Angular 2 - Create a Component

@Component({ selector: 'tb-lineup', template: TEMPLATE, changeDetection: ChangeDetectionStrategy.OnPush, directives: [REACTIVE_FORM_DIRECTIVES]})export class Lineup { @Input() lineup: ILineup[]; @Output() partyJoined: EventEmitter<any> = new EventEmitter(); @Output() partyLeft: EventEmitter<any> = new EventEmitter();

};

Angular 2 - Create a Template

<tr *ngFor="let party of lineup"> <td>{{party.partyId}}</td> <td>{{party.numberOfPeople}}</td> <td>{{party.partyName}}</td> <td> <button type="button" (click)="partyLeft.emit({partyId: party.partyId})">X </button> </td></tr>

Angular 2 - Container Component

• Knows about Redux

• Responsible for getting the data from the store

• Responsible for passing down data

• Responsible for dispatching actions

• Nest where appropriate

@Component({ selector: 'tb-home', template: `<tb-lineup [lineup]="lineup$ | async" (partyJoined)="partyJoined($event)" (partyLeft)="partyLeft($event)"> </tb-lineup>`, directives: [Lineup, RioContainer, Panel, Table, Menu]})export class HomePage { // ... }

Angular 2 - Container Template

export class HomePage {

constructor(private _ngRedux: NgRedux<IAppState>, private _lineupActions: LineupActions) { }

partyJoined({numberOfPeople, partyName}) { this._lineupActions.joinLine(numberOfPeople, partyName);

}

partyLeft({partyId}) { this._lineupActions.leaveLine(partyId);

}

Angular 2 - Container Class

Angular 2 - ngRedux.select

export class HomePage { constructor(private _ngRedux: NgRedux<IAppState>) { } /* ... */ ngOnInit() { this.lineup$ = this._ngRedux.select('lineup');

this.lineupByFunction$ = this._ngRedux.select(state => state.lineup);

this.observable = this._ngRedux .select(state=>state.lineup) .map(line=>line.filter(n => n.numberOfPeople >= 4)) .combineLatest(/*....*/) }

Angular 2 - @select

export class HomePage { @select() lineup$: Observable<ILineup[]>; @select('lineup') lineupByKey$: Observable<ILineup[]>; @select(state => state.lineup) lineupByFunction$: Observable<ILineup[]>;}

Angular 2 - Dispatching Actions

• ngRedux.dispatch

• Works with Redux middleware

• Can dispatch from component, or ActionServices

• Final action that is sent to the reducer is a plain JSON object

Angular 2 - From a Component

@Component({ /* ... */})export class HomePage {

constructor(private _ngRedux: NgRedux<IAppState>) { }

partyJoined({numberOfPeople, partyName}) { this._ngRedux.dispatch<any>(joinLine(numberOfPeople, partyName)); } };

import { joinLine } from ‘../actions';

Angular 2 - ActionServices

• Injectable services

• Can access your other Angular 2 Services

• Access to NgRedux and it’s store methods

• subscribe

• getState

• dispatch

• etc …

Angular 2 - Synchronous Actions

@Injectable()export class LineupActions { constructor(private _ngRedux: NgRedux<IAppState>, private _party: PartyService) { }

leaveLine(partyId) { this._ngRedux.dispatch({ type: PARTY_LEFT, payload: { partyId } }); }}

Handling Asyncronous Actions• Call an action creator to initiate a request.

• Action creator emits an action to inform everyone that a request was made.

• Action creator emits an action to inform everyone that a request was completed. (Or when it fails.)

• Push details of making a request into a module.

Angular 2 - Async Actions

@Injectable()export class LineupActions { constructor(private _ngRedux: NgRedux<IAppState>, private _party: PartyService) { }

joinLine(numberOfPeople, partyName) { this._ngRedux.dispatch({ type: PARTY_JOINING }); this._party.getNextPartyId() .then(partyId => this._ngRedux.dispatch({ type: PARTY_JOINED, payload: { partyId, numberOfPeople, partyName }}) ).catch(err => { this._ngRedux.dispatch({ type: PARTY_JOIN_ERR,

payload: err }); }); };}

Demo Time

Demo• TrendyBrunch

• https://github.com/e-schultz/ng2-camp-example

Caveats• Its addictive.

• You won’t be happy using anything else.

• Your friends might not understand your obsession.

THANK YOU!

Evan Schultz

@e_p82

e-schultz

Developer, rangle.io