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