24. Router store

In this section we will use NgRx's router store library to listen to url or router events as actions in our effects.

1. npm i @ngrx/router-store

npm i @ngrx/router-store

2. Add to AppModule

  • Add StoreRouterConnectingModule to the AppModule.

  • Often it is needed to create your own router serilaizer but we will skip this step.

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { EffectsModule } from '@ngrx/effects';
import { StoreRouterConnectingModule } from '@ngrx/router-store';

import { HomeComponent } from './home/containers/home/home.component';
import { AppComponent } from './app.component';
import { InMemoryDataService } from './app.db';
import { reducer } from './state/spinner/spinner.reducer';
import { environment } from '../environments/environment.prod';

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: '', pathMatch: 'full', redirectTo: 'home' },
      { path: 'home', component: HomeComponent },
      { path: 'event', loadChildren: './event/event.module#EventModule' }
    ]),
    HttpClientModule,
    HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
      delay: 1000
    }),
    StoreModule.forRoot({ spinner: reducer }),
    EffectsModule.forRoot([]),
    StoreDevtoolsModule.instrument({
      name: 'NgRx Demo App',
      logOnly: environment.production
    }),
    StoreRouterConnectingModule.forRoot(),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

3. Add a select box to EventComponent to update URL

  • Add select box element.

src/app/event/event.component.html
<app-add-attendee (addAttendee)="addAttendee($event)"></app-add-attendee>
<select name="Guests filter" (change)="navigate($event.target.value)">
  <option [value]="'all'">All</option>
  <option [value]="'withGuests'">With Guests</option>
  <option [value]="'withoutGuests'">Without Guests</option>
</select>
<app-event-list *ngIf="!(spinner$ | async)" [attendees]="attendees$ | async"></app-event-list>

4. Add navigate method to component

  • Add a navigate method to the component to update the URL.

  • Delete dispatched LoadAttendees action.

src/app/event/event.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';

import { Attendee } from '../../../models';
import { EventService } from '../../services/event.service';
import {
  StartSpinner,
  StopSpinner
} from '../../../state/spinner/spinner.actions';
import { getSpinner } from '../../../state/spinner/spinner.selectors';
import {
  LoadAttendees,
  AddAttendee
} from '../../state/attendees/attendees.actions';
import { EventState } from '../../state';
import { getAttendees } from '../../state/attendees/attendees.selectors';
import { Router } from '@angular/router';

@Component({
  selector: 'app-event',
  templateUrl: './event.component.html',
  styleUrls: ['./event.component.scss']
})
export class EventComponent implements OnInit {
  spinner$: Observable<boolean>;
  attendees$: Observable<Attendee[]>;

  constructor(
    private store: Store<EventState>,
    private eventService: EventService,
    private router: Router
  ) {}

  ngOnInit() {
    this.attendees$ = this.store.pipe(select(getAttendees));
  }

  addAttendee(attendee: Attendee) {
    this.store.dispatch(new AddAttendee(attendee));
  }

  navigate(filterBy: string) {
    this.router.navigateByUrl(`/event?filterBy=${filterBy}`);
  }
}

5. Add next action creator for filterBy property

  • Add new AttendeesActionTypes for FilterBy.

  • Add new class for FilterBy

  • Add new AttendeesActions for FilterBy.

src/app/event/state/attendees/attendees.actions.ts
import { Action } from '@ngrx/store';
import { Attendee } from '../../../models';
export enum AttendeesActionTypes {
  LoadAttendees = '[Attendees Page] Load Attendees',
  LoadAttendeesSuccess = '[Attendees Page] Load Attendees Success',
  LoadAttendeesFail = '[Attendees Page] Load Attendees Fail',
  AddAttendee = '[Attendee Page] Add Attendee',
  AddAttendeeSuccess = '[Attendee API] Add Attendee Success',
  AddAttendeeFail = '[Attendee API] Add Attendee Fail',
  FilterBy = '[Attendee Page] FilterBy'
}
export class LoadAttendees implements Action {
  readonly type = AttendeesActionTypes.LoadAttendees;
}
export class LoadAttendeesSuccess implements Action {
  readonly type = AttendeesActionTypes.LoadAttendeesSuccess;
  constructor(public payload: Attendee[]) {}
}
export class LoadAttendeesFail implements Action {
  readonly type = AttendeesActionTypes.LoadAttendeesFail;
  constructor(public payload: any) {}
}

export class AddAttendee implements Action {
  readonly type = AttendeesActionTypes.AddAttendee;
  constructor(public payload: Attendee) {}
}

export class AddAttendeeSuccess implements Action {
  readonly type = AttendeesActionTypes.AddAttendeeSuccess;
  constructor(public payload: Attendee) {}
}

export class AddAttendeeFail implements Action {
  readonly type = AttendeesActionTypes.AddAttendeeFail;
  constructor(public payload: any) {}
}

export class FilterBy implements Action {
  readonly type = AttendeesActionTypes.FilterBy;
  constructor(public payload: string) {}
}

export type AttendeesActions =
  | FilterBy
  | AddAttendee
  | AddAttendeeSuccess
  | AddAttendeeFail
  | LoadAttendees
  | LoadAttendeesSuccess
  | LoadAttendeesFail;

6. Add an effect to listen to router events

  • Add an effect listening to RouterEvents that begin with /events.

src/app/event/state/attendees.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { ofType } from '@ngrx/effects';
import { switchMap, map, catchError, filter, tap } from 'rxjs/operators';
import { of } from 'rxjs';

import { EventService } from '../../services/event.service';
import {
  AttendeesActionTypes,
  LoadAttendees,
  LoadAttendeesSuccess,
  LoadAttendeesFail,
  AddAttendee,
  AddAttendeeSuccess,
  AddAttendeeFail,
  FilterBy
} from './attendees.actions';
import { Attendee } from '../../../models';
import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { RouterNavigationAction } from '@ngrx/router-store';

@Injectable()
export class AttendeesEffects {
  constructor(private actions$: Actions, private eventService: EventService) {}

  @Effect()
  getAttendees$ = this.actions$.pipe(
    ofType(AttendeesActionTypes.LoadAttendees),
    switchMap((action: LoadAttendees) =>
      this.eventService.getAttendees().pipe(
        map((attendees: Attendee[]) => new LoadAttendeesSuccess(attendees)),
        catchError(error => of(new LoadAttendeesFail(error)))
      )
    )
  );

  @Effect()
  addAttendee$ = this.actions$.pipe(
    ofType(AttendeesActionTypes.AddAttendee),
    switchMap((action: AddAttendee) =>
      this.eventService.addAttendee(action.payload).pipe(
        map((attendee: Attendee) => new AddAttendeeSuccess(attendee)),
        catchError(error => of(new AddAttendeeFail(error)))
      )
    )
  );

  @Effect()
  loadDiaryHealthActions$ = this.actions$.pipe(
    ofType(ROUTER_NAVIGATION),
    map((r: RouterNavigationAction) => ({
      url: r.payload.routerState.url,
      filterBy: r.payload.routerState.root.queryParams['filterBy']
    })),
    filter(({ url, filterBy }) => url.startsWith('/event')),
    map(({ filterBy }) => new FilterBy(filterBy))
  );
}

7. Update reducer to have new filterBy

  • Add switch case to set filterBy property.

src/app/event/state/attendees/attendees.reducer.ts
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';

import { AttendeesActions, AttendeesActionTypes } from './attendees.actions';
import { Attendee } from '../../../models';

export interface State extends EntityState<Attendee> {
  loading: boolean;
  error: any;
  filterBy: string;
}

const adapter: EntityAdapter<Attendee> = createEntityAdapter<Attendee>();

export const intitalState: State = adapter.getInitialState({
  loading: false,
  error: null,
  filterBy: 'all'
});

export function reducer(state = intitalState, action: AttendeesActions): State {
  switch (action.type) {
    case AttendeesActionTypes.LoadAttendees: {
      return adapter.removeAll({
        ...state,
        loading: false,
        error: null
      });
    }

    case AttendeesActionTypes.LoadAttendeesSuccess: {
      return adapter.addAll(action.payload, {
        ...state,
        loading: false,
        error: null
      });
    }

    case AttendeesActionTypes.LoadAttendeesFail: {
      return adapter.removeAll({
        ...state,
        loading: false,
        error: action.payload
      });
    }

    case AttendeesActionTypes.AddAttendeeSuccess: {
      return adapter.addOne(action.payload, { ...state, error: null });
    }

    case AttendeesActionTypes.AddAttendeeFail: {
      return { ...state, error: action.payload };
    }

    case AttendeesActionTypes.FilterBy: {
      return { ...state, filterBy: action.payload };
    }

    default: {
      return state;
    }
  }
}

export const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal
} = adapter.getSelectors();

8. Add new selector for filtering attendees.

  • Add selector to get filterBY state property.

  • Use two getAttendees and getFilterBy to filter the list.

src/app/event/state/attendees/attendees.selectors.ts
---------- ABBREVIATED CODE SNIPPET ----------

export const getFilterBy = createSelector(
  getAttendeeState,
  state => state.filterBy
);

export const getFilteredAttendees = createSelector(
  getAttendees,
  getFilterBy,
  (attendees, filterBy) =>
    attendees.filter(
      attendee =>
        filterBy === 'all'
          ? true
          : filterBy === 'withGuests'
            ? attendee.guests >= 1
            : attendee.guests === 0
    )
);

---------- ABBREVIATED CODE SNIPPET ----------

9. Use new selector in EventComponent

  • Swap getAttendees selector for getFilteredAttendees selector.

src/app/event/containers/event/event.component.ts
---------- ABBREVIATED CODE SNIPPET ----------

  ngOnInit() {
    this.attendees$ = this.store.pipe(select(getFilteredAttendees));
    this.store.dispatch(new LoadAttendees());
  }

---------- ABBREVIATED CODE SNIPPET ----------

Last updated