ngrx - Part 2 Installation and Reducers

Overview

Time: 5min

In this lesson, we will take the CRM app and convert the company list page to use ngrx. In the following lessons, we will explore using more parts of ngrx like action creators and effects.
We will not convert the whole application and hope that this module will inspire you to want to use ngrx in your large or complex applications and will be able to convert the rest of the CRM app with the knowledge you learn in this module.

Learning Outcomes

  • How to install ngrx
  • How to create a reducer
  • How to configure ngrx
  • How to select state from the store and bind these into your HTML templates with the async pipe
  • How to dispatch actions against reducers to update the store
  • How to avoid mutating state in your app and especially reducers

Code for the Completed Lesson

To obtain the code for the completed lesson execute the following git commands.

git clone 'https://firebootcamp:Angular2rocks!@firebootcamp.visualstudio.com/FireBootCamp.Angular/_git/firebootcamp-crm-ngrx'

cd firebootcamp-crm-ngrx

npm install

npm install ngrx

Time: 10min

Like most JavaScript libraries the best way to install and manage them is through npm

  • install npm by running the following command
ng add @ngrx/store@latest

Create a company reducer and simple LOAD_COMPANIES action

Time: 5min

Before we can configure ngrx in our AppModule, we first need to make at least one reducer to configure it with. In these lessons, we will make only a companyReducer, but in a larger application, you will make many reducers one for each related section of the state. Remember the reducer is a pure function, accepting two arguments, the previous state and an action with a type and optional data (payload) associated with the event.

  • Make a new folder called reducers underneath the src/app folder
  • Make a file called company.reducer.ts in the reducers folder

Notice the reducer is just a function that takes in the current state and the action to change the state. The reducer then implements a switch statement that then updates the state. It is always a good idea to initialise the default state versus leaving it null so that in general your team expects an initial default state as normal.

Here we have a single action that can be dispatched LOAD_COMPANIES which we will declare as a constant so we can import it around the application. In another lesson, we will implement another way of achieving this with action creators that will give us better type safety, refactoring and terser code for dispatching events.

In this reducer currently if an event of LOAD_COMPANIES is dispatched the reducer will update the store with the actions payload which will be a new array of Companies. It is important to note reducers are immutable, meaning you always create a new object or array when mutating state versus mutating an existing version. You should not push or pop items onto an array for example but rather create a new array with the new properties. To do this, you will need to be proficient at many of the new ES6 array methods like find, filter, map and also the new spread operators for both arrays and object. You will get into a whole bunch of headaches with the stale state being polluted with the new state if you do not strictly follow this principle, so much so many teams utilise a free or immutable.js library to throw errors when team members accidentally mutate state.

src/app/reducers/company.reducer.ts

import { Company } from './../company/company';
import { createReducer, on } from '@ngrx/store';
import { loadCompanies } from './company.actions';

export const initialState: Company[] = [];

export const companyReducer = createReducer(
  initialState,
  on(loadCompanies, (state, { payload }) => payload),
);

src/app/reducers/company.actions.ts

import { createAction } from '@ngrx/store';
import { Company } from '../company/company';

export const loadCompanies = createAction('[Companies] Load', (companies: Company[]) => ({ payload: companies }));

How to configure ngrx in the AppModule

Time: 5min

src/app/app.module.ts

import { StoreModule } from '@ngrx/store';
import { companyReducer } from './reducers/company.reducer';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    CoreModule,
    routing,
    StoreModule.forRoot({ companies: companyReducer }, {})
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Create a interface for the AppState

Time: 5min

To be able to have strong typing and the benefits of TypeScript in our ngrx code we need to add some interfaces for our ngrx code.

  • Add a new folder called models inside the core folder.
  • Add a new interface called appState for our ngrx code.

src/app/models/appState.ts

import { Company } from '../../company/company';

export interface AppState {
    companies: Company[];
}

Subscribe to the store and replace the CompanyListComponents approach to getting the companies

Time: 5min

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';

import { AppState } from '../../models/appState';
import { Company } from '../company';
import { CompanyService } from '../company.service';

@Component({
  selector: 'app-company-list',
  templateUrl: './company-list.component.html',
  styleUrls: ['./company-list.component.css']
})
export class CompanyListComponent implements OnInit {
  companies$: Observable<Company[]>;

  constructor(
    private store: Store<AppState>,
    private companyService: CompanyService) {
    this.companies$ = this.store.select(state => state.companies);
  }

  ngOnInit() {
    this.loadCompanies();
  }

  loadCompanies() {
    this.companyService.loadCompanies();
  }

  deleteCompany(companyId: number) {
    this.companyService.deleteCompany(companyId);
  }
}

Update the CompanyComponent to use an async pipe

Time: 5min
  • Update the CompanyListComponent to use an async pipe.

When using ngrx, you will notice you unwrap your observables with the async pipe a lot more. Deciding where to unwrap your observables and .subscribe( ) can be challenging. A very logical place is when you are passing data into a presentational component with property binding as in the below example.

  • Update the companies list to use

src/app/company/company-list.component.html

<app-company-table [companies]="companies$ | async " 
(deleteCompany)="deleteCompany($event)"></app-company-table>

Update the CompanyService to dispatch events

Time: 5min
  • Update the CompanyService to loadCompanies

src/app/company/company.service.ts


  loadCompanies() {
    this.httpClient
      .get<Company[]>(`${this.API_BASE}/company`)
      .pipe(
        tap((x) => console.log('TAP - Service', x)),
        finalize(() => console.log('Finalize: Complete')),
        catchError((e) => this.errorHandler<Company[]>(e))
      )
      .subscribe((companies) =>
        this.store.dispatch(loadCompanies(companies))
      );
  }

Summary

Time: 3min

In this lesson, we have added the ngrx library to the CRM App and implemented the redux pattern for adding companies.