Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
SANS
GERARD
Spoken at 103 events in 27 countries
900
1.6K
Rob Wormald
source: blog
1
2
3
4
5
actions
store
A
A
A
S
S
S
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
]
})
export class AppModule {}
{
type: '[Counter] Increment',
}
{
type: '[Todo] Add Todo',
payload: {
id: 1,
text: 'Learn French',
completed: false
}
}
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;
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
// }
// }
// 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
}
}
// 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 }
// 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() { }
}
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)
);
}
}
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);
}
}
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument(),
]
})
export class AppModule {}
@ngrx/store-log-monitor
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 {}
> npm install @ngrx/schematics --save-dev
> ng generate $type $options --collection @ngrx/schematics
> ng set defaults.schematics.collection=@ngrx/schematics
> ng generate $type $options
> ng generate $type $options
> $type: store st, feature f, effect ef, container co
reducer r, action a, entity en
> $options: --flat (default) --group --spec
> $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
> $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
import { Action } from '@ngrx/store';
export enum CounterActionTypes {
CounterAction = '[Counter] Action'
}
export class Counter implements Action {
readonly type = CounterActionTypes.CounterAction;
}
export type CounterActions = Counter;
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);
1
2
3
4
5
@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 new todo
{
type: ADD_TODO,
id: 1,
text: "learn redux",
completed: false
}
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'
// }
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
// }
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
};
{
todos: [{
id: 1,
text: "learn redux",
completed: false
}],
currentFilter: 'SHOW_ALL'
}
source: blog
1
2
3
4
5
A
A
import { EffectsModule } from '@ngrx/effects';
import { todoEffects } from './todoEffects';
@NgModule({
imports: [
StoreModule.forRoot(rootReducer),
EffectsModule.forRoot([todoEffects]),
]
})
export class AppModule {}
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
}))
)
}
// 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
}))
)
@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 }
})
)
)
}
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 { }
// 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
}
{
type: 'ROUTER_NAVIGATION',
payload: {
routerState: {
url: '/users/34;flag=true?q=1',
params: {
id: '34',
flag: 'true'
},
queryParams: {
q: '1'
}
},
event: { /* previous routerState */ }
}
}
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());
}
}
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) {}
}
export class Back implements Action {
readonly type = RouterActionTypes.Back;
}
export class Forward implements Action {
readonly type = RouterActionTypes.Forward;
}
export type Actions = Go | Back | Forward;
@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())
);
}
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 { }
> $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
// 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;
// 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>
// 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();
// 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);
// 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;
// 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;
}
}
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
};
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[]
);
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);
}
}
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 {}
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>
onPush
<todo-app                          (new)="newTodo($event)"
 (toggle)="toggleTodo($event)">
</todo-app>Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â
<add-todo></add-todo>
<todo-list></todo-list>
new
toggle
// 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] : [];
// 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);
};
}
// 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);
};
}
@MikeRyanDev
@robwormald
@brandontroberts
@toddmotto