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()) 1export 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 }); 0Components
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...
2first execution
we keep results and arguments
> const $add = memoise(add);> $add(1,1)cached
2next 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...
2Unless 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
2we 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