evan schultz - angular camp - ng2-redux
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.
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.
Unidirectional Data Flow with Flux
Component
Store
API, etc.
Component
Dispatcher
Action(s)ActionCreator
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
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• 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.