Advanced
NgRx v7+
Advanced
NgRx v7+
Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
SANS
GERARD
Google Developer Expert
Google Developer Expert
International Speaker
Spoken at 113 events in 27 countries
Blogger
Blogger
Community Leader
900
1.6K
Trainer
Master of Ceremonies
Master of Ceremonies
#cfpwomen
NGRX
- v6.1.2 (Dec 2015)
- 146 contributors
- CLI integration
- 4K stars
- v3.3.2 (Jan 2018)
- 86 contributors
- Plugins
- 1.6K stars
- v4 (June 2015)
- 646 contributors
- Inspired by Flux
- 45.5K 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: 3,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!
😃
Best Practices
Project Structure
-
Create folders to group files
- by category (actions, effects, reducers)
- by feature (search, auth)
Component Types
-
Stateful
- Smart Components
- Containers
-
Stateless
- Dumb Components
- Presentational
New Paradigms
- Containers vs Presentational
-
Containers
- Subscribe to the Store
- Dispatch Actions
- Effects for server-side
Container
Presentational
<todo-app>
<todo-list [todos]="todos$ | async"></todo-list>
Top-down Communication
onPush
<todo-app (new)="newTodo($event)"
(toggle)="toggleTodo($event)">
</todo-app>
<add-todo></add-todo>
<todo-list></todo-list>
new
toggle
Events Delegation
High Order Observables
switchMap vs mergeMap vs concatMap
More
@MikeRyanDev
@robwormald
@brandontroberts
Rob Wormald
Mike Ryan
Brandon Roberts
@toddmotto
Todd Motto
Advanced NgRx (v7+)
By Gerard Sans
Advanced NgRx (v7+)
In this talk we are going to uncover some advanced patterns used in ngrx. The Selector Pattern helps us transform any existing data in our State creating infinite possibilities using a generic approach. While looking into different use cases we will also explore Memoization and how to implement custom Selectors. In this talk we are going to use a basic application to showcase the ngrx/store. Finally we will cover more complex scenarios including selectors and implementing custom memoization.
- 3,028