Selectors & Memoization in ngrx v6
by Gerard Sans | @gerardsans
 
     
    Google Developer Expert

Google Developer Expert
International Speaker
 
    Spoken at 95 events in 25 countries
 
    Blogger
Blogger
Community Leader
 
    900
1.5K
Trainer
 
    Master of Ceremonies

Master of Ceremonies





 
     
         
         
         
         
         
    NGRX
- v6.0.1 (Dec 2015)
- 125 contributors
- CLI integration
- 3K stars
- v3.2 (Jan 2018)
- 66 contributors
- Plugins
- 1K stars
 
    - v4 (June 2015)
- 603 contributors
- Inspired by Flux
- 42K 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...
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
1-slot Memoisation Limitations
> $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
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
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

😃




@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);
  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
 
             
         
        
    Selector Pattern and Memoisation in ngrx v6
By Gerard Sans
Selector Pattern and Memoisation in ngrx v6
State Management is key to build modern Web Apps
- 5,440
 
    

 
   
   
  