Unlocking the NgRx Platform
Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
Unlocking the NgRx Platform
SANS
GERARD
Google Developer Expert
Google Developer Expert
International Speaker
Spoken at 103 events in 27 countries
Blogger
Blogger
Community Leader
900
1.6K
Trainer
Master of Ceremonies
Master of Ceremonies
#cfpwomen
ngrx
Rob Wormald
@ngrx/platform
-
Suite including
- @ngrx/store
- @ngrx/effects
- @ngrx/router-store
- @ngrx/store-devtools
- @ngrx/entity
- @ngrx/schematics
Benefits
Why use ngrx?
-
Developer Experience
- Predictability
- Traceability
- Automation
- Performance
- Testability
ngrx/store
ngrx/store
- Re-implementation of Redux on top Angular and RxJS
-
Colocation Support
- StoreModule.forRoot()
- StoreModule.forFeature()
- Supports Lazy Load
- Type safety
@ngrx/store life cycle
source: blog
1
2
3
4
5
actions
store
A
A
A
S
S
S
ngrx/store Setup
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
]
})
export class AppModule {}
Actions
- Actions may have a payload
- Asynchronous actions are called effects
- Typed Actions
Action Examples
{
type: '[Counter] Increment',
}
{
type: '[Todo] Add Todo',
payload: {
id: 1,
text: 'Learn French',
completed: false
}
}
Action Creators
- Enum declares each action type
- Each action has a supporting class
- Action type is a readonly property
- If needed, payload is handled in the constructor
Action Creators Example
import { Action } from '@ngrx/store';
export enum ActionTypes {
Increment = '[Counter] Increment',
Reset = '[Counter] Reset',
}
export class Increment implements Action {
readonly type = ActionTypes.Increment;
}
export class Decrement implements Action {
readonly type = ActionTypes.Reset;
constructor(public payload: { value: number });
}
export type Actions = Increment | Reset;
Action Creators Example
import * as counter from './counter.actions';
const action = new counter.Increment();
// {
// type: '[Counter] Increment'
// }
const action = new counter.Reset({ value: 0 });
// {
// type: '[Counter] Reset'
// payload: {
// value: 0
// }
// }
Reducers
- Same as in Redux
- Typed Actions and State
- Composable using ActionReducerMap<State>
Example: reducer (1/2)
// counter.reducer.ts
export const initialState: number = 0;
export function reducer(
state = initialState,
action: CounterActions
): number {
switch (action.type) {
// add your actions here
default:
return state; // forward state to next reducer
}
}
Example: reducer (2/2)
// counter.reducer.ts
export function reducer(state = 0, action:CounterActions): number {
switch (action.type) {
case CounterActionTypes.Increment: return state + 1;
case CounterActionTypes.Decrement: return state - 1;
default:
return state;
}
}
// execution
reducer(undefined, { type:'@ngrx/store/init' }) // { counter: 0 }
store.dispatch( new counter.Increment() );
reducer(0, { type:'[Counter] Increment' }) // { counter: 1 }
Containers
Container Component
// loading.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromStore from '../reducers/index';
@Component({
selector: 'app-loading',
templateUrl: './loading.component.html',
styleUrls: ['./loading.component.css']
})
export class LoadingComponent implements OnInit {
constructor(private store: Store<fromStore.State>) { }
ngOnInit() { }
}
Basic Selector
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
@Component({
template: `<div *ngIf="loading | async"><svg></svg></div>`
})
export class LoadingComponent implements OnInit {
loading: Observable<boolean>;
ngOnInit() {
this.loading = this.store.pipe(
map(state => state.loading)
);
}
}
select Example
import { Observable } from 'rxjs/Observable';
@Component({
template: `<div *ngIf="loading | async"><svg></svg></div>`
})
export class LoadingComponent implements OnInit {
loading: Observable<boolean>;
ngOnInit() {
this.loading = this.store.select('loading');
// this.loading = this.store.select(state => state['loading']);
// this.loading = this.store.select(state => state.loading);
}
}
ngrx/store-devtools
ngrx/store-devtools
- Redux DevTools for ngrx
- Live debugging
- State and Actions tracking
- Dispatch Actions
- Save/Restore State
- Time Travel
ngrx/store-devtools Setup
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument(),
]
})
export class AppModule {}
Instrumentation Options
- Set Inspector name
- Set maximum history
- Implement Custom Monitor
-
@ngrx/store-log-monitor
-
- Enable State/Action Sanitizers
- Disable features
Instrumentation Example
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({
name: 'my-app',
maxAge: 25,
features: { pause: false, export: false, jump: false, test: true },
}),
]
})
export class AppModule {}
extension.remotedev.io
ngrx/schematics
ngrx/schematics
- Scaffolding templates for ngrx
- Integrates with Angular CLI
-
Provides commands for
- Setting initial Store and Effects
- Actions, Reducers, Entities, Features
- Containers + Store injected
- Automates creation + registration
ngrx/schematics Setup
> npm install @ngrx/schematics --save-dev
> ng generate $type $options --collection @ngrx/schematics
> ng set defaults.schematics.collection=@ngrx/schematics
> ng generate $type $options
Blueprints ngrx
> ng generate $type $options
> $type: store st, feature f, effect ef, container co
reducer r, action a, entity en
> $options: --flat (default) --group --spec
Initial Store Setup
> $project: ngrx-basic
> $state: State
> ng new $project
> npm install @ngrx/schematics --save-dev
> npm install @ngrx/{store, effects, entity, store-devtools} --save
> ng generate store $state --root --module app.module.ts
New Action Example
> $name: counter
// same folder no specs
> ng generate action $name
create src/app/counter.actions.ts
// separate actions folder + specs
> ng generate action $name --group --spec
create src/app/actions/counter.actions.ts
create src/app/actions/counter.actions.spec.ts
// same folder + specs
> ng generate action $name --spec
create src/app/counter.actions.ts
create src/app/counter.actions.spec.ts
counter.actions.ts
import { Action } from '@ngrx/store';
export enum CounterActionTypes {
CounterAction = '[Counter] Action'
}
export class Counter implements Action {
readonly type = CounterActionTypes.CounterAction;
}
export type CounterActions = Counter;
Example
Setup
import { App } from './app';
import { StoreModule } from "@ngrx/store";
import { rootReducer } from './rootReducer';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(rootReducer)
],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
Adding a new Todo
- Component subscribes
- Component dispatches ADD_TODO action
- Store executes rootReducer
- Store notifies Component
- View updates
1
2
3
4
5
Subscribing to the Store
@Component({
template:
`<todo *ngFor="let todo of todos | async">{{todo.text}}</todo>`
})
export class App implements OnDestroy {
public todos: Observable<Todo>;
constructor(
private _store: Store<TodosState>
) {
this.todos = _store.let(
state$ => state$.select(s => s.todos);
);
}
}
ADD_TODO Action
// add new todo
{
type: ADD_TODO,
id: 1,
text: "learn redux",
completed: false
}
todos Reducer
const initialState = [];
const todos = (state = initialState, action:Action) => {
switch (action.type) {
case TodoActions.ADD_TODO:
return state.concat({
id: action.id,
text: action.text,
completed: action.completed });
default: return state;
}
}
// {
// todos: [], <-- todos reducer will mutate this key
// currentFilter: 'SHOW_ALL'
// }
currentFilter Reducer
const currentFilter = (state = 'SHOW_ALL', action: Action) => {
switch (action.type) {
case TodoActions.SET_CURRENT_FILTER:
return action.filter
default: return state;
}
}
// {
// todos: [],
// currentFilter: 'SHOW_ALL' <-- filter reducer will mutate this key
// }
rootReducer
import { ActionReducerMap } from '@ngrx/store';
interface Todo {
id: number,
text: string,
completed: boolean
}
export interface TodosState {
todos: Array<Todo>;
currentFilter: string;
}
export const rootReducer: ActionReducerMap<TodosState> = {
todos: todos,
currentFilter: currentFilter
};
New State
{
todos: [{
id: 1,
text: "learn redux",
completed: false
}],
currentFilter: 'SHOW_ALL'
}
ngrx/effects
ngrx/effects
- Asynchronous Actions Middleware
- Executes side-effects
-
Colocation Support
- EffectsModule.forRoot()
- EffectsModule.forFeature()
Asynchronous Actions
source: blog
1
2
3
4
5
A
A
ngrx/effects Setup
import { EffectsModule } from '@ngrx/effects';
import { todoEffects } from './todoEffects';
@NgModule({
imports: [
StoreModule.forRoot(rootReducer),
EffectsModule.forRoot([todoEffects]),
]
})
export class AppModule {}
Effects Service
import { Actions, Effect, ofType } from '@ngrx/effects';
import { TodoActions, ADD_TODO_EFFECT } from './todoActions';
@Injectable()
export class todoEffects {
constructor(
private actions$ : Actions,
private actions : TodoActions ) { }
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
)
}
Add new todo async
// Dispatch
private addTodoAsync(input) {
this._store.dispatch({
type: ADD_TODO_EFFECT
text: input.value
});
input.value = '';
}
// Middleware
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
)
Error Handling
@Injectable()
export class todoEffects {
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
.catch(error => Observable.of({
type: 'ADD_TODO_FAILED',
error: { message: error }
})
)
)
}
ngrx/router-store
ngrx/router-store
- Bindings for Angular Router
- Router emits Actions
- ROUTER_NAVIGATION
- ROUTER_CANCEL
- ROUTER_ERROR
- Available to Reducers and Effects
- Throwing errors cancel navigation
ngrx/router-store Setup
import { StoreRouterConnectingModule,
RouterStateSerializer } from '@ngrx/router-store';
import { CustomRouterStateSerializer } from './utils/CustomRouterStateSerializer';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
],
providers: [
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }
],
})
export class AppModule { }
Router Reducer Setup
// reducers/index.ts
import { RouterStateUrl } from '../utils/CustomRouterStateSerializer';
import * as fromRouter from '@ngrx/router-store';
export interface State {
router: fromRouter.RouterReducerState<RouterStateUrl>;
}
export const reducers : ActionReducerMap<State> = {
router: fromRouter.routerReducer
}
Custom Router Serializer
{
type: 'ROUTER_NAVIGATION',
payload: {
routerState: {
url: '/users/34;flag=true?q=1',
params: {
id: '34',
flag: 'true'
},
queryParams: {
q: '1'
}
},
event: { /* previous routerState */ }
}
}
Enabling
Router Actions
Router Actions
import { Store } from '@ngrx/store';
import * as fromStore from './reducers/index';
import * as router from './actions/router.actions';
export class ContainerComponent {
constructor(private store: Store<fromStore.State>) {
this.store.dispatch(new router.Go({
path: ['/users', 34, { flag: true }],
query: { q: 1 },
extras: { replaceUrl: false }
}));
this.store.dispatch(new router.Back());
this.store.dispatch(new router.Forward());
}
}
Router Actions Setup (1/2)
import { NavigationExtras } from '@angular/router';
export enum RouterActionTypes {
Go = '[Router] Go',
Back = '[Router] Back',
Forward = '[Router] Forward'
}
export interface NavigationUrl {
path: any[];
query?: object;
extras?: NavigationExtras;
}
export class Go implements Action {
readonly type = RouterActionTypes.Go;
constructor(public payload: NavigationUrl) {}
}
Router Actions Setup (2/2)
export class Back implements Action {
readonly type = RouterActionTypes.Back;
}
export class Forward implements Action {
readonly type = RouterActionTypes.Forward;
}
export type Actions = Go | Back | Forward;
Router Effects Setup (1/2)
@Injectable()
export class RouterEffects {
constructor(private actions$: Actions, private router: Router,
private location: Location) {}
@Effect({ dispatch: false })
navigate$ = this.actions$.pipe(
ofType(RouterActionTypes.Go),
pluck('payload'),
tap(({ path, query: queryParams, extras}) =>
this.router.navigate(path, { queryParams, ...extras }))
)
@Effect({ dispatch: false })
navigateBack$ = this.actions$.pipe(
ofType(RouterActionTypes.Back),
tap(() => this.location.back())
);
}
Router Effects Setup (2/2)
import { EffectsModule } from '@ngrx/effects';
import { RouterEffects } from './effects/router.effects'
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
EffectsModule.forRoot([RouterEffects]),
],
providers: [
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }
],
})
export class AppModule { }
ngrx/entity
ngrx/entity
- Library to manage Lists
-
High performance
- EntityState<T>
- Ids lookup and Entities Map
-
Reducer Helper (EntityAdapter)
- getInitialState
- Add/update/remove: All, One, Many
- getSelectors: Ids, Entities, All, Total
ngrx/schematics Entity
> $name: todo
> ng generate entity $name --module app.module.ts
create src/app/todo.actions.ts
create src/app/todo.model.ts
create src/app/todo.reducer.ts
create src/app/todo.reducer.spec.ts
update src/app/app.module.ts
Todos Actions
// src/app/reducers/todo/todo.actions.ts
export enum TodoActionTypes {
AddTodo = '[Todo] Add Todo', UpdateTodo = '[Todo] Update Todo', ...
}
let currentId = 1;
export class AddTodo implements Action {
readonly type = TodoActionTypes.AddTodo;
constructor(public payload: { todo: Todo }) {
payload.todo.id = currentId++;
}
}
export type TodoActions = LoadTodos | AddTodo | UpsertTodo | AddTodos
| UpsertTodos | UpdateTodo | UpdateTodos | DeleteTodo | DeleteTodos | ClearTodos;
Todos Model
// src/app/reducers/todo/todo.model.ts
import { EntityState } from '@ngrx/entity';
export interface Todo {
id: number;
completed: boolean;
text: string;
}
export interface Todos {
todos: EntityState<Todo>;
}
{
todos: [
{
id: 1,
text: 'Learn ngrx',
completed: false
}
],
currentFilter: 'SHOW_ALL'
}
{
todos: {
ids: [
1
],
entities: {
'1': {
id: 1,
text: 'Learn ngrx',
completed: false
}
}
},
currentFilter: 'SHOW_ALL'
}
todo: Array<Todo>
todo: EntityState<Todo>
Todo Reducer
// src/app/reducers/todo/todo.reducer.ts
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export function reducer(
state = initialState,
action: TodoActions
): State {
switch (action.type) {
case TodoActionTypes.AddTodo: {
return adapter.addOne(action.payload.todo, state);
}
default:
return state;
}
}
export const {
selectIds, selectEntities, selectAll, selectTotal
} = adapter.getSelectors();
Setting initialState
// src/app/reducers/todo/todo.reducer.ts
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export let initialState: State = adapter.getInitialState({
// additional entity state properties
});
initialState = adapter.addMany([
{ id: -2, text: 'Learn ngrx', completed: true },
{ id: -1, text: 'Try Cherry Liquor 😋', completed: false }
], initialState);
Filter Actions
// src/app/reducers/currentFilter/currentFilter.actions.ts
import { Action } from '@ngrx/store';
export enum CurrentFilterActionTypes {
SetCurrentFilter = '[Filter] Set current filter'
}
export class SetCurrentFilter implements Action {
readonly type = CurrentFilterActionTypes.SetCurrentFilter;
constructor(public payload: { filter: string }) {}
}
export type CurrentFilterActions = SetCurrentFilter;
Filter Reducer
// src/app/reducers/currentFilter/currentFilter.reducer.ts
export const initialState: string = 'SHOW_ALL';
export function reducer(
state = initialState,
action: CurrentFilterActions): string
{
switch (action.type) {
case CurrentFilterActionTypes.SetCurrentFilter:
return action.payload.filter
default:
return state;
}
}
Composing Reducers
import * as todos from './todo/todo.reducer';
import * as currentFilter from './currentFilter/currentFilter.reducer';
export interface Filter {
currentFilter: string;
}
export interface Todos {
todos: EntityState<Todo>;
}
export interface TodosState extends Todos, Filter { }
export const reducers: ActionReducerMap<TodosState> = {
todos: todos.reducer,
currentFilter: currentFilter.reducer
};
Selectors
export const selectTodos = (state: TodosState) => state.todos;
export const selectCurrentFilter = (state: TodosState) => state.currentFilter;
export const selectTodosEntities = createSelector(
selectTodos,
todoEntity.selectAll // (todos: EntityState<Todo>): Todo[]
);
export const getFilteredTodos = createSelector(
selectTodosEntities,
selectCurrentFilter,
getVisibleTodos // (todos: Todo[], filter: string): Todo[]
);
Container
import { TodosState, getFilteredTodos } from './reducers'
@Component({
template: `<todo *ngFor="let todo of todos | async"></todo>`
})
export class AppComponent implements OnInit {
todos: Observable<Todo[]>;
constructor(private store: Store<TodosState>) { }
public ngOnInit() {
this.todos = this.store.select(getFilteredTodos);
}
}
AppModule Setup
import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
import { reducers } from './reducers';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(reducers),
!environment.production ? StoreDevtoolsModule.instrument() : [],
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {}
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
Service
Component
Service
Component
Service
Component
Service
Component
Service
Component
Service
Component
Service
Component
Component
Component
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
Advanced ngrx
Meta Reducers
MetaReducers
- Execute before Reducers
-
High order function
- ActionReducer<State>
- Used for logging, storage
-
Composable using
- ​ActionReducer<State>[]
Metareducers Setup
// app.module.ts
import { StoreModule } from '@ngrx/store';
import { reducers, metaReducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, { metaReducers }),
]
})
export class AppModule {}
// reducers/index.ts
import { environment } from '../../environments/environment';
import { meta } from './meta.reducer';
export const metaReducers = !environment.production ? [meta] : [];
Example: metareducer (1/2)
// meta.reducer.ts
import { ActionReducer, Action } from '@ngrx/store';
import { State } from './index';
export function meta(
reducer: ActionReducer<State>
): ActionReducer<State> {
return function(state: State, action: Action): State {
return reducer(state, action);
};
}
Example: metareducer (2/2)
// meta.reducer.ts
import { ActionReducer, Action } from '@ngrx/store';
import { State } from './index';
export function meta(
reducer: ActionReducer<State>
): ActionReducer<State> {
return function(state: State, action: Action): State {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
};
}
More
@MikeRyanDev
@robwormald
@brandontroberts
Rob Wormald
Mike Ryan
Brandon Roberts
@toddmotto
Todd Motto
Thanks
Thanks
Unlocking the NgRx platform
By Gerard Sans
Unlocking the NgRx platform
During this hands-on day we will cover all ngrx packages (v5) so you can master the whole ngrx platform including: store, effects, router, entity and devtools. You will learn how to integrate the ngrx platform in your current projects following best practices!
- 3,726