20. Create effect

In this section we will discuss NgRx's effects library a RxJS powered side effect model for @ngrx/store.

@ngrx/effects provides an API to model event sources as actions. Effects:

  1. Listen for actions dispatched from @ngrx/store.

  2. Isolate side effects from components, allowing for more pure components that select state and dispatch actions.

  3. Provide new sources of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events.

1. npm install @ngrx/effects

  • Execute the following command to install the effects library.

npm i @ngrx/effects

2. Dispatch new LoadAttendees action from the EventComponent

Before we can use effect we need to dispatch some actions from our EventComponent for our effects to listen to. We will not need the service in our component when we finish but for now we still need it to add Attendees.

  • Dispatch actions to LoadAttendees.

  • Use the new EventState to limit the auto completion of what we are meant to be able to access from the store. Note we can still access everything as the store is a global object.

  • Remove getAttendees method which will now be handled by the effect we are about to write.

src/app/event/containers/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 } from '../../state/attendees/attendees.actions';
import { State } from '../../state';

@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<State>,
    private eventService: EventService
  ) {}

  ngOnInit() {
    this.store.dispatch(new LoadAttendees());
    this.spinner$ = this.store.pipe(select(getSpinner));
    this.attendees$ = this.store.pipe(select(state => state.event.attendees.attendees));
  }

  addAttendee(attendee: Attendee) {
    this.store.dispatch(new StartSpinner());
    this.eventService.addAttendee(attendee).subscribe(() => {
      this.store.dispatch(new StopSpinner());
    });
  }
}

3. Create an effect

Effects are all about listening for actions doing work and dispatching new actions. So in our example we will listen for the LoadAttendees action do the work of getting them via a service and then dispatch a LoadAttendeesSuccess action.

NgRx effects are a deep dive into observables and if you have not done a lot with RxJS or reactive streams in other languages can take some time to step through.

  • Create and attendees.effects.ts file in our state/attendees folder path.

  • Inject the actions observable from NgRx that will emit each action dispatched in our application and the EventService to get the attendees.

  • Add an @Effect decorator on top of a variable name of the effect.

  • List the injected actions and filter on them with the ofType operator from NgRx.

  • Use the switchMap operator to switch from the actions stream to a new observable returned from our EventService and return an LoadAttendeesSuccess action.

  • Add a catchError operator and return an observable of LoadAttendeesFail with a payload of the error.

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

import { EventService } from '../../services/event.service';
import {
  AttendeesActionTypes,
  LoadAttendees,
  LoadAttendeesSuccess,
  LoadAttendeesFail
} from './attendees.actions';
import { Attendee } from '../../../models';

@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)))
      )
    )
  );
}

4. Update reducer to listen for success and fail actions

  • Add an error property to the State object to hold any errors.

  • Update the attendee reducer to update the store with the attendees array form the affect.

  • Add a case for the LoadAttendeesFail action and set its payload to the error property of the state object.

src/app/event/state/attendees/attendees.reducer.ts
import { Attendee } from '../../../models';
import { AttendeesActions, AttendeesActionTypes } from './attendees.actions';

export interface State {
  attendees: Attendee[];
  loading: boolean;
  error: any;
}

export const intitalState: State = {
  attendees: [],
  loading: false,
  error: null
};

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

    case AttendeesActionTypes.LoadAttendeesSuccess: {
      return {
        ...state,
        loading: false,
        attendees: action.payload,
        error: null
      };
    }

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

    default: {
      return state;
    }
  }
}

5. Add effects array to index.ts file for EventModule

Our index.ts file in our event state folder is our public API so we want to add our different feature states effect to.

  • Re-export effects from the index.ts.

src/app/event/state/index.ts
import { ActionReducerMap } from '@ngrx/store';

import * as fromRoot from './../../state/state';
import * as fromAttendees from './attendees/attendees.reducer';
import { AttendeesEffects } from './attendees/attendees.effects';

export interface EventState {
  attendees: fromAttendees.State;
}

export interface State extends fromRoot.State {
  event: EventState;
}

export const reducers: ActionReducerMap<EventState> = {
  attendees: fromAttendees.reducer
};

export const effects: any[] = [AttendeesEffects];

6. Register the feature effects in the EventModule

  • Register the effects with the EffectsModule.forFeature method.

src/app/event/event.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';

import { EventComponent } from './containers/event/event.component';
import { AddAttendeeComponent } from './components/add-attendee/add-attendee.component';
import { EventListComponent } from './components/event-list/event-list.component';
import { reducers, effects } from './state';

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([{ path: '', component: EventComponent }]),
    ReactiveFormsModule,
    HttpClientModule,
    StoreModule.forFeature('event', reducers),
    EffectsModule.forFeature(effects)
  ],
  declarations: [EventComponent, AddAttendeeComponent, EventListComponent]
})
export class EventModule {}

7. Add default EffectsModule registration to root AppModule

For us to use effects in our app and reducers we need to have a root reducer and effect to start with. The root reducer could just be an object and the effects an empty array but we need to have them. The feature effects and reducers we make are then added to them as we lazily load them.

  • Add forRoot with an empty array for the AppModule.

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 { 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
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
  • Even before we write selectors we can see our app should working using our new effects.

8. Create a getAttendees selector

Similar to making selectors for our spinner we will now make them for our attendees reducer. You will end up having a selectors file for each reducer and they can become very logic heavy and need unit test but for now ours are quite simple.

  • Create selector for selecting attendees.

src/app/event/state/attendees/attendees.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { EventState } from '..';

export const getEventState = createFeatureSelector<EventState>('event');

export const getAttendeeState = createSelector(
  getEventState,
  state => state.attendees
);

export const getAttendees = createSelector(
  getAttendeeState,
  state => state.attendees
);

9. Use selector in EventComponent

  • Use getAttendees selector.

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

  ngOnInit() {
    this.spinner$ = this.store.pipe(select(getSpinner));
    this.attendees$ = this.store.pipe(select(getAttendees));
    this.store.dispatch(new LoadAttendees());
  }
  
---------- ABBREVIATED CODE SNIPPET ----------

RxJS Operators and higher order observables

It is important to know which higher order observables to use with your effects to avoid race conditions. A higher order observable is just a fancy name for an observable that emits observable like the ones below.

Operator

When to use

switchMap

Cancels the current subscription/request and can cause race condition

Use for get requests or cancelable requests like searches

concatMap

Runs subscriptions/requests in order and is less performant

Use for get, post and put requests when order is important

mergeMap

Runs subscriptions/requests in parallel

Use for put, post and delete methods when order is not important

exhaustMap

Ignores all subsequent subscriptions/requests until it completes

Use for login when you do not want more requests until the initial one is complete

Extras and Homework

These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises.

WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course.

You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you.

Try out the @ngrx/schematics to scaffold out an application.

Schematics are Angular's way of being able to have custom code generators. You can read more here https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2

You will be wondering why we did not do this from the start and save us a lot of typing? Well first you need to know what you are doing before you can use the scaffolding tools as many students get lost in all the code it makes versus building it up from from nothing for the first time.

Steps:

  1. Run this command to install the schematics npm i @ngrx/schematics -D

  2. Run this command to set them as the default schematics ng config cli.defaultCollection @ngrx/schematics

  3. Run this command to make a default empty root store. ng g store State --root --statePath state --module app.module.ts

  4. Run this command to make a container component ng generate container Event2 --state reducers/index.ts --stateInterface Event2

  5. Run this command to make the new feature module ng g module party ng g c party/containers/party --module party/party.module.ts

  6. Run this command to make the NgRx feature parts like reducer and effects etc ng generate feature party/party --flat false

Last updated