NgRx Course – NgRx Effects and Selectors

In the previous video we introduced the store devtools. We created some actions and we wrote the reducer for the customer feature. In this video, we’re going to create a side effect for our application. We’ll manage it using NgRx/effects.

We’ve already seen how the components dispatch actions. Then the reducer takes the action and the current state and returns a new state. The reducers are pure functions and we want to keep them pure. But some actions create side effects in our application. For example, when we communicate with an external service like a back-end server.

In order to handle those side effects, we use NgRx/effects in our application. These effects listen for an action being dispatched. They then use a service to communicate to the server. Once they get a response they dispatch another action that will be handled by the reducer.

So, for example, when we want to get all the customers from the server, the component will dispatch an action called LOAD_CUSTOMERS. The effect will take that action, communicate with the server, fetch the data and dispatch a new action called LOAD_CUSTOMERS_SUCCESS. The reducer which then will return a new state. Once the state is replaced in the store, the components will be notified. The templates will be updated with the loaded customers.

Let’s implement this in our application. We’ll start with the customer.effect.ts . First, we make a service and call it CustomerEffect:

// customers/state/customer.effect.ts
import { Injectable } from "@angular/core";

@Injectable()
export class CustomerEffect {
  constructor(
  ) {}
}

In the constructor, we’re going to inject the CustomerService and the Actions from the store. This is so we can listen to them:

// customers/state/customer.effects.ts (updated)
import { Injectable } from "@angular/core";
import { Actions } from "@ngrx/effects";
import { CustomerService } from "../customer.service";

@Injectable()
export class CustomerEffect {
  constructor(
     private actions$: Actions,
     private customerService: CustomerService
  ) {}
}

Now we’re going to register the effect using the @Effect() decorator. We’re going to give it the name loadCustomers$. This is going to be of type Observable.

// customers/state/customer.effects.ts (updated)
import { Injectable } from "@angular/core";
import { Actions ,  Effect } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs";
import { CustomerService } from "../customer.service";

@Injectable()
export class CustomerEffect {
  constructor(
     private actions$: Actions,
     private customerService: CustomerService
) {}
@Effect()
  loadCustomers$: Observable<Action> = this.actions$
}

We want to return some data from the action. We can use the pipe method to combine operators to get the data that we want. So let’s use it:

// customers/state/customer.effects.ts (updated)
import { Injectable } from "@angular/core";
import { Actions ,  Effect , ofType } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs";
import { mergeMap } from "rxjs/operators";
import { CustomerService } from "../customer.service";
import * as customerActions from "../state/customer.actions";

@Injectable()
export class CustomerEffect {
  constructor(
     private actions$: Actions,
     private customerService: CustomerService
) {}
@Effect()
  loadCustomers$: Observable<Action> = this.actions$.pipe(
    ofType<customerActions.LoadCustomers>(
      customerActions.CustomerActionTypes.LOAD_CUSTOMERS
    ),
    mergeMap()
  );
}

Here we’re specifying that we want to listen to an action of type LOAD_CUSTOMERS. When that action happens we use the mergeMap() operator to map over the action and return the result of calling our CustomerService, like the following:

// customers/state/customer.effects.ts (updated)
import { Injectable } from "@angular/core";
import { Actions ,  Effect , ofType } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable, of } from "rxjs";
import { mergeMap, map, catchError} from "rxjs/operators";
import { Actions ,  Effect , ofType } from "@ngrx/effects";
import { CustomerService } from "../customer.service";
import { Customer } from "../customer.model";
import * as customerActions from "../state/customer.actions";

@Injectable()
export class CustomerEffect {
  constructor(
     private actions$: Actions,
     private customerService: CustomerService
) {}
@Effect()
  loadCustomers$: Observable<Action> = this.actions$.pipe(
    ofType<customerActions.LoadCustomers>(
      customerActions.CustomerActionTypes.LOAD_CUSTOMERS
    ),
    mergeMap((action: customerActions.LoadCustomers) =>
      this.customerService.getCustomers().pipe(
        map(
          (customers: Customer[]) =>
            new customerActions.LoadCustomersSuccess(customers)
        ),
        catchError(err => of(new customerActions.LoadCustomersFail(err)))
      )
    )
  );
}

We use another pipe to map over the results of the customer service. Then return a new LoadCustomersSuccess action passing in the customers as the payload. Once we have the effect, we need to add it to the customers.module.ts file:

// customers/state/customer.module.ts
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";

/* NgRx */
import { StoreModule } from "@ngrx/store";
import { EffectsModule, Actions } from "@ngrx/effects";
import { customerReducer } from "./state/customer.reducer";
import { CustomerEffect } from "./state/customer.effects";

import { CustomerComponent } from "./customer/customer.component";
import { CustomerAddComponent } from "./customer-add/customer-add.component";
import { CustomerEditComponent } from "./customer-edit/customer-edit.component";
import { CustomerListComponent } from "./customer-list/customer-list.component";

// Routes
const customerRoutes: Routes = [{ path: "", component: CustomerComponent }];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(customerRoutes),
    StoreModule.forFeature("customers", customerReducer),
    EffectsModule.forFeature([CustomerEffect])
  ],
  declarations: [
    CustomerComponent,
    CustomerAddComponent,
    CustomerEditComponent,
    CustomerListComponent
  ]
})
export class CustomersModule {}

And then we have to add it to the app.module.ts file:

// app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";

// NgRx
import { StoreModule } from "@ngrx/store";
import { EffectsModule } from "@ngrx/effects";

import { AppComponent } from "./app.component";
import { HomeComponent } from "./home/home.component";
import { NavbarComponent } from "./navbar/navbar.component";
import { AppRoutingModule } from "./app-routing.module";

import { StoreDevtoolsModule } from "@ngrx/store-devtools";

@NgModule({
  declarations: [AppComponent, HomeComponent, NavbarComponent],
  imports: [
    BrowserModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
    StoreDevtoolsModule.instrument(),
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Let’s update the component to load the customers when it’s initialized. So, open the customer-list.component.ts. We already have an action from our previous example. But we’re going to dispatch it using our recently created action type:

// customers/customer-list.component.ts
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";

// import customer
import * as customerActions from "../state/customer.actions";

@Component({
  selector: "app-customer-list",
  templateUrl: "./customer-list.component.html",
  styleUrls: ["./customer-list.component.css"]
})
export class CustomerListComponent implements OnInit {
  customers;

  constructor(private store: Store<any>) {}

  ngOnInit() {
    this.store.dispatch(new customerActions.LoadCustomers());
    this.store.subscribe(state => (this.customers = state.customers.customers));
  }
}

We import the customer actions and dispatch the new action when the component is initialized. Now if we refresh the page we should see the changes in the store. The component will have been updated with the list of customers.

In this example, we’re subscribing to the state directly from our component. As our application grows, it becomes more complex to get what we want from the store. So instead of this approach we are going to use selectors.

Selectors are functions that subscribe to slices of the state and communicate the changes to the components. When the state is updated, the components that are subscribed to the selector will be notified.

Let’s create the selectors in our customer.reducer.ts file. First, we need to import createFeatureSelector and createSelector from ngrx/store:

// customers/customer.reducer.ts
import * as customerActions from "./customer.actions";
import { Customer } from "../customer.model";

import * as fromRoot from "../../state/app-state";
import { createFeatureSelector, createSelector } from "@ngrx/store";
...
// (to be continued)
The we’re going to add the selector functions at the bottom of the file:

// customers/customer.reducer.ts (continued)
…
const getCustomerFeatureState = createFeatureSelector<CustomerState>(
  "customers"
);

export const getCustomers = createSelector(
  getCustomerFeatureState, 
  (state: CustomerState) => state.customers
);

export const getCustomersLoading = createSelector(
  getCustomerFeatureState,
  (state: CustomerState) => state.loading
);

export const getCustomersLoaded = createSelector(
  getCustomerFeatureState,
  (state: CustomerState) => state.loaded
);

export const getError = createSelector(
  getCustomerFeatureState,
  (state: CustomerState) => state.error
);

We are creating a feature selector and assigning it to a constant. Passing into the createFeatureSelector() function, the name of the slice of state that we want to select. Then we pass it as the first argument of the createSelector() functions and return the data that we want from that slice of state.

Now that we have the selectors we can subscribe in our component:

// customers/customer.component.ts
import { Component, OnInit } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { Observable } from "rxjs";

import * as customerActions from "../state/customer.actions";
import * as fromCustomer from "../state/customer.reducer";
import { Customer } from "../customer.model";

@Component({
  selector: "app-customer-list",
  templateUrl: "./customer-list.component.html",
  styleUrls: ["./customer-list.component.css"]
})
export class CustomerListComponent implements OnInit {
  customers$: Observable<Customer[]>;

  constructor(private store: Store<fromCustomer.AppState>) {}

  ngOnInit() {
    this.store.dispatch(new customerActions.LoadCustomers());
    this.customers$ = this.store.pipe(select(fromCustomer.getCustomers));
  }
}

Here we import the necessary dependencies and we subscribe to the store using the selector for the specific slice of state that we want. We also improved our code slightly by adding the type to the customers$ observable and specifying the AppState interface for the Store. Now, all we have to do in our template is add the async pipe. This makes sure that it unsubscribes automatically once the component is destroyed.

// customers/customer.component-list.html
...
  <tbody>
    <tr>
      <tr *ngFor="let customer of (customers$ | async)">
        <th scope="row">{{customer.name}}</th>
        <td>{{customer.phone}}</td>
        <td>{{customer.address}}</td>
        <td>{{customer.membership}}</td>
        <th>
          <a>edit</a>
          <br>
          <a>delete</a>
        </th>
      </tr>
  </tbody>
...

In the next video, we’ll introduce the NgRx/entity library and will continue building the CRUD functionality of our application.