Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
SANS
GERARD
Spoken at 113 events in 27 countries
900
1.6K
source: blog
1
2
3
4
5
actions
store
A
A
A
S
S
S
Components
Actions
Reducer
Match
Dispatch
Creates
State
Notify
@ngrx/store-devtools
@ngrx/store
@ngrx/schematics
@ngrx/router-store
@ngrx/effects
@ngrx/entity
Features
Utilities
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
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
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
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
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
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
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
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
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
app/app.module.ts
{
counter: 7
}
{
type: "[Counter] Increment"
}
State
Action
User clicks
{
counter: 8
}
{
"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
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
app/reducers/named.reducer.ts
namedReducer(fromCounter.reducer, "counter-2")
{
type: "[Counter] Decrement"
payload: { target: "counter-2" }
}
State
Property Selectors
Computed Selectors
s
s
s
Helpers to access Store
Avoid Components tight coupling with Store
Colocated with Reducers
store.select(state => state.total);
store.select(state => state.pagination.page);
Components
State
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
app/reducers/index.ts
State
Property Selectors
Computed Selectors
s
s
s
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
// State
{
todos: [{ id: 1, text: 'Learn ngrx', complete: false }],
currentFilter: "SHOW_COMPLETED"
}
// Visible Todos
[]
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;
app/reducers/index.ts
src/app.component.ts
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
app/todos.component.ts
export const getBooksState =
createFeatureSelector<BooksState>('books');
// {
// search: ...
// books: ...
// collection: ...
// }
This is always from the Root
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('books', reducers),
],
})
export class BooksModule { }
src/app.component.ts
app/books/books.module.ts
Books Module can be lazy loaded
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
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
> $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
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
😃
Uses 1-slot Memoisation
createSelector, createFeatureSelector
src/app.component.ts
> 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: 3,maxAge: 60000});
return { memoized, reset: () => memoized.clear() }
};
const $createSelector = createSelectorFactory(customMemoizer);
export const getFilteredTodos = $createSelector(
selectTodosEntities,
selectCurrentFilter,
getVisibleTodos
);
src/app.component.ts
We can use deepEqual for arguments
reset will clear
the cache
src/app.component.ts
> 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!
😃
Container
Presentational
<todo-app>
<todo-list [todos]="todos$ | async"></todo-list>
onPush
<todo-app (new)="newTodo($event)"
(toggle)="toggleTodo($event)">
</todo-app>
<add-todo></add-todo>
<todo-list></todo-list>
new
toggle
@MikeRyanDev
@robwormald
@brandontroberts
@toddmotto