Single Page Apps
Mobile Apps
As the app gets bigger:
Dan Abramov
The state of your whole application is stored in an object tree within a single store.
The only way to change the state is to emit an action, an object describing what happened.
To specify how the state tree is transformed by actions, you write pure reducers.
Depend only on input parameters
Have no observable side effects
What that means:
let reducer = (prev, curr) => {
return prev + curr;
}
[1, 2, 3, 4].reduce( reducer, 0 ); // => 10Execution:
arr[0] : reducer( 0, 1 ) -> 1
arr[1] : reducer( 1, 2 ) -> 3
arr[2] : reducer( 3, 3 ) -> 6
arr[3] : reducer( 6, 4 ) -> 10Example:
(state: T, action: Action) => TExample:
function counterReducer (state: number = 0, action: Action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};Definition:
interface Action {
type: string;
payload?: any;
}Examples
// Simple increment
lat incrementAction = { type: "INCREMENT" };// Complex action with payload
lat setValue = {
type: "SET",
payload: { value: 2000 }
};All the information describing and action in one object
Current State
Action
Reducer
New State
Initial State
Action
Reducer
State
Action
Reducer
State
Action
Reducer
State
Use Object.assign to create new object instances.
Use Object.freeze()
var list1 = Immutable.List.of(1, 2);
var list2 = list1.push(3, 4, 5); // new list
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50); // new mapExample:
And also:
const initialState = [0, 0, 0, 0, 0, 0, 0, 0, 0];
export function boardReducer(
state: Array<number> = initialState,
action: Action): Array<number> {
switch (action.type) {
case PLAY_X:
return setTile(state, action.payload, 1);
case PLAY_O:
return setTile(state, action.payload, -1);
case FINISH:
return initialState;
default:
return state;
}
};
function setTile(board: Array<number>, pos: number, val: number): Array<number> {
return [
...board.slice(0, pos),
val,
...board.slice(pos + 1, board.length)];
}
export interface Score {
xWins: number;
oWins: number;
draws: number;
}
const initialState = { xWins: 0, oWins: 0, draws: 0};
export function scoreReducer(score: Score = initialState, action: Action): Score {
if (action.type === FINISH) {
switch (action.payload.winner) {
case 0:
return Object.assign({}, score, { draws: score.draws + 1 });
case 1:
return Object.assign({}, score, { xWins: score.xWins + 1 });
case -1:
return Object.assign({}, score, { oWins: score.oWins + 1 });
}
return score;
}import { StoreModule, combineReducers } from '@ngrx/store';
import { scoreReducer } from './score/score.reducer';
import { boardReducer } from './board/board.reducer';
let rootReducer = combineReducers({
board: boardReducer,
score: scoreReducer
});
interface AppState {
board: Array<number>;
score: Score;
}
@NgModule({
declarations: [AppComponent, ...],
imports: [
// ...
StoreModule.provideStore(rootReducer),
],
bootstrap: [AppComponent]
})
class AppModule { };import {Store} from '@ngrx/store';
interface AppState {
board: Array<number>;
score: Score;
}
@Component({ ... })
export class AppComponent implements OnDestroy {
board$: Observable<Array<number>>;
score$: Observable<Score>;
boardFull$: Observable<Score>;
constructor( public store: Store<AppState> ) {
this.board$ = store.select(s => s.board.present);
this.score$ = store.select(s => s.score);
this.boardFull$ = this.board$.map(b => !b.some(val => val === 0));
}
}<grid-layout rows="auto, auto, auto, auto, *">
...
<grid-layout row="2" class="board">
<tic-board [board]="board$ | async"
(positionSelected)="positionSelected($event)"></tic-board>
</grid-layout>
<grid-layout row="3" class="board">
<tic-score [score]="score$ | async"></tic-score>
</grid-layout>
<grid-layout *ngIf="boardFull$ | async"
rowSpan="4" rows="auto *" class="popup">
<!-- Show this when the game ends -->
</grid-layout>
</grid-layout><grid-layout rows="auto, auto, auto, auto, *">
...
<tic-board [board]="board$ | async"
(positionSelected)="positionSelected($event)"></tic-board>
...
</grid-layout>export class AppComponent {
constructor(public store: Store<AppState>) { ... }
// ...
positionSelected(position: number) {
this.store.dispatch({
type: this.currentPlayer ? PLAY_X : PLAY_O,
payload: position
});
}
}
@Component({
selector: "tic-board",
template: `
<wrap-layout itemWidth="50" itemHeight="50" width="150" height="150">
<button class="tile"
*ngFor="let val of board; let i = index"
[text]="val | player"
(tap)="!val && action.next(i)" >
</button>
</wrap-layout>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BoardComponent {
@Input() board: Array<number>;
@Output() positionSelected = new EventEmitter<number>();
}
Logging
Crash reporting
Provide Undo/Redo functionality
Persist state
function logger(reducer) {
return function (state, action) {
console.log('---- DISPATCHED ACTION: ' + JSON.stringify(action));
// Calculate next state using original reducer
let nextState = reducer(state, action);
console.log('---- NEW STATE: ' + JSON.stringify(nextState));
return nextState;
};
};let rootReducer = combinerReducers({...});
rootReducer = logger(rootReducer);logger.meta-reducer.ts
Wrap the rootReducer:
import { combineReducers } from '@ngrx/store';
import { scoreReducer } from './score/score.reducer';
import { boardReducer } from './board/board.reducer';
let rootReducer = combineReducers({
board: boardReducer,
score: scoreReducer
});Use combineReducer to assemble a root reducer:
describe('CoutnerReducer', () => {
it('INCREMENT should increment', () => {
const oldState = 0;
const action = { type: INCREMENT };
const newState = 1;
assert.equal(newState, counterReducer(oldState, action));
});
it('DECREMENT should decrement', () => {
const oldState = 0;
const action = { type: DECREMENT };
const newState = -1;
assert.equal(newState, counterReducer(oldState, action));
});
});