Advanced NgRx
by Gerard Sans | @gerardsans

Advanced NgRx




SANS
GERARD
Google Developer Expert

Google Developer Expert
International Speaker

Spoken at 100 events in 26 countries

Blogger
Blogger
Community Leader

900
1.5K
Trainer

Master of Ceremonies

Master of Ceremonies













FREE 3h Workshop
bit.ly/cfp-sampa

NGRX
- v6.0.1 (Dec 2015)
- 129 contributors
- CLI integration
- 3K stars
- v3.2 (Jan 2018)
- 69 contributors
- Plugins
- 1K stars

- v4 (June 2015)
- 611 contributors
- Inspired by Flux
- 43K stars
Overview
- State Management for Angular
- Inspired by Redux
- Implemented using RxJS
- Angular CLI integration via schematics
@ngrx/store life cycle
source: blog

1
2
3
4
5
actions
store
A
A
A
S
S
S

Components
Actions
Reducer
Match
Dispatch
Creates
State
Notify
Packages
@ngrx/store-devtools
@ngrx/store
@ngrx/schematics
@ngrx/router-store
@ngrx/effects
@ngrx/entity
Features
Utilities
Counter
Increment
Decrement
Reset
Total
Components
Actions
Reducer
Match
Dispatch
Creates
State
Notify
import { Action } from '@ngrx/store';
export enum CounterActionTypes {
Increment = '[Counter] Increment',
}
export class CounterIncrement implements Action {
readonly type = CounterActionTypes.Increment;
}
export type CounterActions = CounterIncrement;
src/app.component.ts
Increment Action
app/actions/counter.actions.ts
> new CounterIncrement();
{
type: "[Counter] Increment"
}

export function reducer(state = 0, action: CounterActions): number
{
switch (action.type) {
case CounterActionTypes.Increment:
return state + 1;
default:
return state;
}
}
src/app.component.ts
Counter Reducer
app/reducers/counter.reducer.ts
> reducer(undefined, { type: "@ngrx/store/init" })
0
> reducer(0, new CounterIncrement())
1

export enum CounterActionTypes {
Reset = '[Counter] Reset'
}
export class CounterReset implements Action {
readonly type = CounterActionTypes.Reset;
constructor(public payload: { value: number }) { }
}
export type CounterActions = CounterIncrement | CounterReset;
src/app.component.ts
Reset Action
app/actions/counter.actions.ts
> new CounterReset({ value: 0 });
{
type: "[Counter] Reset",
payload: { value: 0 }
}

export function reducer(state = 0, action: CounterActions): number
{
switch (action.type) {
case CounterActionTypes.Reset:
return action.payload.value;
default:
return state;
}
}
src/app.component.ts
Counter Reducer
app/reducers/counter.reducer.ts
> reducer(42, new CounterReset({ value: 0 });
0

Components
Actions
Reducer
Match
Dispatch
Creates
State
Notify
@Component({
template: `
<app-counter
(increment)="increment()" (decrement)="decrement()"
(reset)="reset()">
</app-counter>`
})
export class AppComponent {
constructor(private store: Store) { }
increment = () => this.store.dispatch(new CounterIncrement());
decrement = () => this.store.dispatch(new CounterDecrement());
reset = () => this.store.dispatch(new CounterReset({ value: 0 }));
}
src/app.component.ts
Dispatching Actions
app/app.component.ts
We pass events up to container

@Component({
template: `
<app-counter [total]="counter|async"></app-counter>`
})
export class AppComponent {
counter: Observable<number>;
constructor(private store: Store<fromStore.State>) {
this.counter = this.store.select(state => state.counter);
}
}
src/app.component.ts
Subscribing to the Store
app/app.component.ts
Maps value
not Observable
We will improve this part later


@Component({
selector: 'app-counter',
template: `
<div class="total">{{total}}</div>
<button (click)="increment.emit()">+</button>
<button (click)="decrement.emit()">-</button>
<button (click)="reset.emit()">C</button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
@Input() total: number;
@Output() increment = new EventEmitter();
...
}
src/app.component.ts
Counter
app/counter/counter.component.ts
better performance

Components
Actions
Reducer
Match
Dispatch
Creates
State
Notify
import * as fromCounter from './counter.reducer';
export interface State {
"counter": number;
}
export const reducers: ActionReducerMap<State> = {
"counter": fromCounter.reducer,
};
src/app.component.ts
Counter State
app/reducers/index.ts
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(reducers),
],
})
export class AppModule { }
src/app.component.ts
Store Setup
app/app.module.ts
Reusing reducers


{
counter: 7
}
Single Counter
{
type: "[Counter] Increment"
}
State
Action
User clicks
{
counter: 8
}

Multiple Counters
{
"counter-1": 7,
"counter-2": 3,
}
{
type: "[Counter] Decrement",
payload: { target: "counter-2" }
}
counter-1
counter-2
State
Action
User clicks
{
"counter-1": 7,
"counter-2": 2,
}

import * as fromCounter from './counter.reducer';
import { namedReducer } from './named.reducer';
export interface State {
"counter-1" : number;
"counter-2" : number;
}
export const reducers: ActionReducerMap<State> = {
"counter-1": namedReducer(fromCounter.reducer, "counter-1"),
"counter-2": namedReducer(fromCounter.reducer, "counter-2")
};
src/app.component.ts
Counter State
app/reducers/index.ts
export function namedReducer(reducer: any, target: string) {
return (state: number, action: CounterActions) => {
// ignore action and return current state
if (action.payload && action.payload.target != target)
return state;
// otherwise use original reducer
return reducer(state, action);
}
}
src/app.component.ts
Meta-reducer
app/reducers/named.reducer.ts
namedReducer(fromCounter.reducer, "counter-2")
{
type: "[Counter] Decrement"
payload: { target: "counter-2" }
}

Selectors

State
Property Selectors
Computed Selectors
s
s
s
Property Selectors
-
Helpers to access Storeβ
-
βAvoid Components tight coupling with Store
-
Colocated with Reducers
store.select(state => state.total);
store.select(state => state.pagination.page);
Tight coupling
Components
State
Selectors: loose coupling
store.select(getCounter());
store.select(getPage());
Components
Selectors
State
A small change won't break all Components now

// counter
export const getCounter =
(state: State): number => state.counter;
// todos
export const getTodos =
(state: State): Array<Todo> => state.todos;
export const getFilter =
(state: State): string => state.currentFilter;
src/app.component.ts
Property Selectors
app/reducers/index.ts
State
Property Selectors
Computed Selectors
s
s
s
Computed Selectors
-
Compute values using other Selectors
-
ββcreateSelector, createFeatureSelector
-
-
Memoised for performance
-
Colocated with Reducers
// State
{
todos: [{ id: 1, text: 'Learn ngrx', complete: false }],
currentFilter: "SHOW_ALL"
}
// Visible Todos
[{ id: 1, text: 'Learn ngrx', complete: false }]
src/app.component.ts
Example: Visible Todos Selector
// State
{
todos: [{ id: 1, text: 'Learn ngrx', complete: false }],
currentFilter: "SHOW_COMPLETED"
}
// Visible Todos
[]
createSelector
export const getTodos = state => state.todos;
export const getFilter = state => state.currentFilter;
export const getVisibleTodos = createSelector(
getTodos,
getFilter,
projector
);
export const getTodos = state => state.todos;
export const getFilter = state => state.currentFilter;
export const getVisibleTodos = createSelector(
getTodos,
getFilter,
(todos, filter) => visibleTodos(todos, filter)
);
export const getTodos = state => state.todos;
export const getFilter = state => state.currentFilter;
export const getVisibleTodos = createSelector(
getTodos,
getFilter,
visibleTodos
);
The projector function is just the calculation we want to run
Refactored!
todos is getTodos(state)
filter is getFilter(state)
export const getTodos = state => state.todos;
export const getFilter = state => state.currentFilter;
Using createSelector
app/reducers/index.ts



src/app.component.ts
Selector: Visible Todos
function visibleTodos(todos: Array<Todo>, filter: string) {
switch (filter) {
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ALL':
default:
return todos;
}
};
import { getVisibleTodos } from './reducers'
@Component({
template: `<todo *ngFor="let todo of todos | async"></todo>`
})
export class TodoComponent {
todos: Observable<Array<Todo>>;
constructor(private store: Store<fromStore.State>) {
this.todos = this.store.select(getVisibleTodos);
}
}
src/app.component.ts
Using getVisibleTodos
app/todos.component.ts
createFeatureSelector
export const getBooksState =
createFeatureSelector<BooksState>('books');
// {
// search: ...
// books: ...
// collection: ...
// }
This is always from the Root
Using createFeatureSelector

import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('books', reducers),
],
})
export class BooksModule { }
src/app.component.ts
Store Setup
app/books/books.module.ts
Books Module can be lazy loaded

Memoisation

Memoisation
-
Created to improve performance of Machine Learning algorithms
-
Targets repetitive calls trading space for speed
-
Uses a Cache to store Results
-
1-slot and multi-slot
src/app.component.ts
1-slot Memoisation
function memoise(fn) {
let cache, $args;
return function() {
if (sameArgs($args, arguments)){
console.log('cached');
return cache;
} else {
console.log('calculating...');
cache = fn(...arguments);
$args = arguments;
return cache;
}
}
};
const add = (a,b) => a+b;
> $add(1,1)
calculating...
2
first execution
we keep results and arguments
> const $add = memoise(add);
> $add(1,1)
cached
2
next execution
with same arguments
we use cache



src/app.component.ts
1-slot Memoisation Limitations
> $add(1,1)
calculating...
2
> $add(2,2)
calculating...
4
> $add(1,1)
calculating...
2
Unless we do consecutive repetitive calls (recursive) is not very effective

π±

src/app.component.ts
Multi-slot Memoisation
function memoise(fn) {
let cache = {};
return function() {
const key = arguments.join('-');
if (key in cache) {
return cache[key];
}
else {
cache[key] = fn(...arguments);
return cache[key];
}
}
};
const add = (a,b) => a+b;
we use the key 1-1 to cache results
> $add(1,1)
calculating...
2
> $add(2,2)
calculating...
4
> $add(1,1)
cached
2
we create a unique key using the arguments
Use JSON.stringify for a generic approach
cache
{"1-1": 2, "2-2": 4}
this time
we hit the cache

π




@ngrx/store Selectors
-
Uses 1-slot Memoisation
-
βcreateSelector, createFeatureSelector
-
- Applied to projectorβ function
-
Allows replacing default memoisation
- reselect (1-slot)
- moize (multi-slot)
src/app.component.ts
1-slot Memoisation: getVisibleTodos
> const state = {
todos: [{ id: 1, text: 'Learn ngrx', complete: false }],
currentFilter: "SHOW_ALL"
};
[{ id: 1, text: 'Learn ngrx', complete: false }]
> getVisibleTodos(state);
filter function executed
[]
> state = { ...state, currentFilter: "SHOW_COMPLETED" }
> getVisibleTodos(state);
> state = { ...state, currentFilter: "SHOW_ALL" }
> getVisibleTodos(state);
[{ id: 1, text: 'Learn ngrx', complete: false }]
executed again
and again...
π±



import { createSelectorFactory } from '@ngrx/store';
import moize from "moize";
const customMemoizer = fn => {
const memoized = moize.deep(fn, {maxSize: 5, maxAge: 60000});
return { memoized, reset: () => memoized.clear() }
};
const $createSelector = createSelectorFactory(customMemoizer);
export const getFilteredTodos = $createSelector(
selectTodosEntities,
selectCurrentFilter,
getVisibleTodos
);
src/app.component.ts
Multi-slot getVisibleTodos
We can use deepEqual for arguments

reset will clear
the cache

src/app.component.ts
Multi-slot Memoisation: getVisibleTodos
> const state = {
todos: [{ id: 1, text: 'Learn ngrx', complete: false }],
currentFilter: "SHOW_ALL"
};
[{ id: 1, text: 'Learn ngrx', complete: false }]
> getVisibleTodos(state);
filter function executed
[]
> state = { ...state, currentFilter: "SHOW_COMPLETED" }
> getVisibleTodos(state);
> state = { ...state, currentFilter: "SHOW_ALL" }
> getVisibleTodos(state);
[{ id: 1, text: 'Learn ngrx', complete: false }]
executed again
Cached!
π



More

@MikeRyanDev
@robwormald
@brandontroberts


Rob Wormald
Mike Ryan
Brandon Roberts
@toddmotto

Todd Motto



Advanced NgRx
By Gerard Sans
Advanced NgRx
State Management is key to build modern Web Apps
- 6,625