Beginner's Guide to ngrx and Angular

Introduction

State management is a term that will always come to mind whenever dealing with an application data structure.

The biggest problem in the development and maintenance of large-scale software systems is complexity - large systems are hard to understand.

Reactive programming is when we react to data being streamed to us over time.

In this article, we are going to cover the fundamentals of ngrx and its applications in Angular.

Source Code

The source code implemented in this article can be found in this GitHub repository.

What is ngrx?

ngrx is a set of RxJS-powered state management libraries for Angular, inspired by Redux, a popular and predictable state management container for JavaScript apps. It was developed by Rob Wormald, an Angular Developer Advocate in 2013.

Here are some of the goodies that ngrx brings to us:

  1. ngrx aims to bring reactive extensions to Angular.
  2. @ngrx/store brings a Redux-like single store for all of your app states to Angular.

ngrx/store is an implementation of Redux that was developed with RxJS while keeping the core concepts and API of Redux.

ngrx, being inspired by Redux, shares the same principles with it and supercharging it with RxJS.

We will go into the inner workings of @ngrx/store in the sections below.

What is RxJS?

RxJS is a JavaScript library for reactive programming that allows you to work with asynchronous or callback-based data streams.

Talking about streams - a stream is a sequence of values in time. This streams of events and data in real-time - which we call Observables - are a beautiful way to handle asynchronous code.

Using RxJS, we would write something like this:

var button = document.querySelector('button');

Rx.Observable.fromEvent(button, 'click')  
    .subscribe(() => console.log('Clicked!'));

var arr = Rx.Observable.of(90, 80)  
    .subscribe((v) => console.log('Value:', v));

You see, with RxJS we can achieve so much with very little code.

What is Redux?

Redux, as stated earlier, is a state management library for JavaScript apps. Although it was initially developed for the React community, it can also be used in vanilla JavaScript or with any other JavaScript framework.

Redux is a library that implements the ideas of Flux. Flux is a design pattern that made popular one-way data flow (unidirectional flow), which was first presented by Facebook.

States of an app in Redux are kept in the store. The states are updated using actions transported to pure functions called reducers. Reducers take in state and action as parameters and performs an immutable action on the state and returns a new state.

Core concepts

Before we dive into the nuts and bolts of how/what makes @ngrx/store work, let's take a look at the core concepts. Applications developed with @ngrx/store must deal with the Store, Reducers, State, and Actions.

Store

To put simply, store is the "database" of our application. It comprises of different states defined in our application. The state, thus, is immutable and only altered by actions.

The store combines the whole application state into a single entity, acting as a database for the web application. Like a traditional database it represents the point of record for an application, your store can be thought of as a client side "single source of truth".

Reducer

If the store is the database of the application, the reducers are the tables. A reducer is a pure function that accepts two parameters - an action and the previous state with a type and optional data associated with the event.

Sample Reducer
export function reducer(state = initialState, action: articles.Actions):State {  
    switch(action.type) {
        case 'ADD_ARTICLE':
            return { 
                articles: [...state.articles,action.payload]
            }
        default:
            return state;
    }
}

State

State is a single immutable data structure. States are what makes up the store. As stated before, the reducers are like tables, and thus state are fields in the table.

Actions

Store encompasses the state of our application and reducers get the state slices or sections of the store, but how do we update the store when the need arises? That is the role of the actions. Actions represent payloads of information that are dispatched to the store from the application and are usually triggered by user interaction.

// Action interface
export interface Action {  
    type: string,
    payload?: any
}

// Action with payload
dispatch({type: 'ADD_ARTICLE', payload: {link: 'github.com/philipszdavido', points:90}})

// Action without payload
dispatch({type:'LOAD_LINKS'})  

When an action is dispatched, the reducer takes it and applies the payload, depending on the action type, and outputs the new state.

To recap a few points: The store encompasses the whole state, the reducers return fragments of the state, and actions are predefined user-triggered events that communicate how a given frame of the state should change.

Advantages of the Store

We have seen how effective and useful @ngrx/store is for managing states in our app. But before we get on to show the application of it in Angular, we'll take a look at its advantages.

The main advantage the store has are Centralized State, Testing, Perfomance, and DevTools.

  • Centralized State: State in a store is kept in one directory. It makes it easier to foretell updates or changes to the store, and track down problems
  • Testing: It is easy to write tests for pure functions. Since the store is composed of reducers - which are pure functions that use only its inputs to produce its outputs with no side effects. Simply input in, and assert the output.
  • Performance: Unidirectional data-flow state changes from its reactivity makes it very fast and efficient.
  • DevTools: Awesome tools have been created to help developers. One example is ngrx/store-devtool, which helps developers "time travel" during development. It also has some nice features that helps provide a history of actions and state changes.

ngrx/store: Behind the Scenes

@ngrx/store was built with the tenets of RxJS. BehaviorSubject, Subject and Observable are RxJS core types that compose the engine of @ngrx/store. Let's understand these concepts first, then we can effectively use the library.

To understand a concept very well, you have to look at the source code. @ngrx/store was, for a long time, a mystery to me until I came to get the picture when I downloaded the project from its Git repo and dove into the source code. I got to see how the library was brilliantly built. In doing so, I really got acquainted with the stuff.

Looking into the code, you'll see that @ngrx/store has four core classes Store, State, ActionsSubject, and ReducerManager that does the main work within the library.

  • Store is where it all starts, it instantiates other classes. It extends the Observable class so that we can subscribe to it to get the latest state.
  • ActionsSubject handles the dispatching of action into the Store.
  • State holds the last emitted state value.
  • ReducerManager holds the reducer function and calls the reducer function with the state value from State and the action from the ActionsSubject class.

ngrx in Practice

Now, its time we show how to use @ngrx/store in Angular. To demonstrate the power of @ngrx/store, we will build a simple "online store", that will allow users to do the following:

  • See a list of products
  • View a particular product
  • Users can add a product to their cart
  • Users can delete a product from their cart

Sample Application

We will use angular/cli to set up our project, which you can install by running the command:

$ npm install angular/cli -g

Here, we installed the angular/cli globally so that we can use it from any directory in our system.

Setup

We are now set. We will call our project folder, "online-store". To scaffold the project, run the command:

$ ng new online-store --minimal

Notice the use of the minimal flag, this is used to create a barebones Angular app. It generates "spec", HTML, and CSS files. Everything will be inline (inside the *.component.ts file).

Now, our directory structure will look this:

├── online-store
  ├── src
    ├── app
      ├── app.component.ts
      └── app.module.ts
    ├── assets
      └── .gitkeep
    ├── environment
      ├── environment.prod.ts
      └── environment.ts
    ├── index.html
    ├── main.ts
    ├── polyfills.ts
    ├── style.css
    ├── tsconfig.app.json
    └── typings.d.ts
  ├── .angular-cli.json
  ├── .gitignore
  ├── package.json
  └── tsconfig.json

Now, we will install Bootstrap to make our app responsive and good-looking:

$ npm install bootstrap -S

Remane "style.css" to "style.scss", then open "style.scss" and add the following line:

@import "~bootstrap/scss/bootstrap.scss"

Next, we pull in the @ngrx/store library:

$ npm install @ngrx/store @ngrx/core -S

Our app will have three components:

  • products.component: This will display lists of products and their prices.
  • cart.component: This component displays all items we have added to the cart.
  • product.component: This component will display the name and details of a product selected.

To scaffold the above components run the following commands:

$ ng g c products --inline-style=true --spec=false
$ ng g c cart --inline-style=true --spec=false
$ ng g c product --inline-style=true --spec=false

Notice the options --inline-style=true --spec=false we passed to the ng g c command. The ng utility has a plethora of options that can be used in Angular to suit your needs.

Here, passing --inline-style=true tells Angular to generate the component's style inside the ts file. --spec=true skips generating the test *.spec.ts file.

Next we are going to add routing to our app. We will create three routes:

  • /products: This will be our index route. It will activate the products.component.
  • /cart: This will activate the cart.component to display the user's cart
  • /product/:id: This route has an id param that will be used to display a particular product.

To enable routing in our app, we have to import the RouterModule and Routes from @angular/router in app.module.ts:

// app.module.ts

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
...
import { Routes, RouterModule } from '@angular/router'  
...

Next, we define a variable routes of type Routes. It will contain an array of our routes:

// app.module.ts

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
...
import { Routes, RouterModule } from '@angular/router'  
...
const routes: Routes = [  
    {
        path: '',
        redirectTo: '/products',
        pathMatch: 'full'
    },
    {
        path: 'products',
        component: ProductsComponent
    },
    {
        path: 'cart',
        component: CartComponent
    },
    {
        path: 'product/:id',
        component: ProductComponent
    },
    {
        path: '**',
        redirectTo: '',
        pathMatch: 'full'
    }
];
...

This represents all possible router states our app can be in.

As we stated earlier, our app has three routes: "products", "products/:id", and "cart". We added some additional configuration here:

  • path: '': This redirects to /products because /products is our index page. We can actually make "" index page by just adding the component property and assigning it to the products.component. It's up to you.
  • path:'**': This will redirect back to /products page if none of the routes match the user's request.

Now, to activate the routing system in our app, we call the RouterModule's forRoot method in the imports array, passing the routes variable as a parameter.

// app.module.ts

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';  
import { ProductsComponent } from './products/products.component';  
import { CartComponent } from './cart/cart.component';  
import { ProductComponent } from './product/product.component';  
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [  
    {
        path: '',
        redirectTo: '/products',
        pathMatch: 'full'
    },
    {
        path: 'products',
        component: ProductsComponent
    },
    {
        path: 'cart',
        component: CartComponent
    },
    {
        path: 'product/:id',
        component: ProductComponent
    },
    {
        path: '**',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
  declarations: [
    AppComponent,
    ProductsComponent,
    CartComponent,
    ProductComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
});

export class AppModule { }  

Finally, we need to tell the Angular Router where it can place our app routing configuration in the DOM.

We will add the <router-outlet></router-outlet> element to AppComponent's template.

// app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
  <p>
    app works!
    <router-outlet></router-outlet>
  </p>
  `,
  styles: []
});

export class AppComponent {  
  title = 'app';
};

The <router-outlet></router-outlet> element tells the Angular Router where to insert the matching component in the DOM.

Define our Reducer

Now we're going to define our store and start attaching reducers to it. Let's create a central folder called "store" for all our store-related files:

$ mkdir src/app/store

OK, now let's create our reducer file "reducer.ts":

$ touch src/app/store/reducer.ts

This file will contain our reducer function, which we will get to later on. As mentioned earlier, some of the features of this app is to handle "removing a product from the cart" and "adding a product to the cart". So our reducer will handle removing and adding products to cart.

Back to what we were saying before, the "reducer.ts" file will contain a reducer function accepting the previous state and the currently dispatched action as parameters. We then need to implement a switch case system to check for the correct action and perform recalculation on the state.

// src/app/store/reducer.ts

import { CartActionTypes, CartActions } from "./actions";

export let initialState = []

export function reducer(state=initialState, action: CartActions) {  
    switch (action.type) {
        case CartActionTypes.ADD_PRODUCT: 
            return [...state, action.payload]
        case CartActionTypes.REMOVE_PRODUCT: 
            let product = action.payload        
            return state.filter((el)=>el.id != product.id)
        default: 
            return state
    }
}

So here is our reducer function. Keep in mind that we do not want to use state changing methods. Immutabilty is the key here.

I'm sure you are wondering about some of the objects in the code above, like CartActionTypes and CartActions. Don't worry, we will get to that in the "Setting up Actions" section.

Initializing the Store

Currently we have only one state in this app, thus only one 'getter' reducer. You can see the drill here, every state item has its own reducer function. In a large and complex app, we can have many reducers, one reducer for each state item in the app. These reducers will be combined to a single reducer function using the combineReducers function.

Now let's put our reducer into the store:

// app.module.ts

...
import { StoreModule } from "@ngrx/store";  
import { reducer } from './store/reducer';

...

@NgModule({
  declarations: [
      ...
  ],
  imports: [
    ...
    StoreModule.forRoot({cart: reducer})
  ],
  providers: [], 
  bootstrap: [AppComponent]
});

export class AppModule { }  

Here we have made our app's store available to the entire app. We can access it anywhere. Looking at the code, we imported the StoreModule from @ngrx/store and our reducer function from src/app/store/reducer.ts file. Then, we called the forRoot method passing {cart: reducer} as the parameter in the imports array.

We passed in an object with the cart property set because since we have only one state, we won't need to consider state slices. So we just tell ngrx to only send us the cart state.

Projecting the State

Now, that we have made our store accessible, we can access one of the states by using the select function.

// app.componen.ts

import { Component } from '@angular/core';  
import { Store, select } from '@ngrx/store';

@Component({
  selector: 'app-root',
  template: `
  <p>
    app works !!
    Cart: {{cart.length}}
    <router-outlet></router-outlet>
  </p>
  `,
  styles: []
})

export class AppComponent {  
  title = 'app';
  cart: Array<any>

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

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.store.select('cart').subscribe((state => this.cart = state))
  }
}

The select function plucks the desired state from the app and returns an Observable, so that we can subscribe to changes made to that state.

Setting up Actions

Actions are the queries into the app's store. They "dispatch" actions to be performed on the store.

First, we will need a model. The basis of our state is an array of products. The product inside the array represents an item a user can buy. The user can add it to cart or remove it.

So, we know how our cart contains products/items, and a product can have the following:

  • name
  • price
  • tag
  • id
  • description

We will select just name, id, price to represent a product here.

Next we'll create a model for our product:

// src/app/store/product.model.ts

export class Product {  
    id: number
    name: string
    price: number
}

Next, we define our actions as custom actions implementing the @ngrx/store Action class.

Instead of dispatching actions like this:

store.dispatch({type: '', payload: ''})  

We create actions as a new class instance:

this.store.dispatch(new Cart.AddProduct(product))  

Expressing actions as classes enables type-checking in reducer functions. To create our actions classes, first we create an enum that will hold our action types. Remember, the only things this app does is "Add To Cart" and "Remove From Cart". So our enum will look like this:

// src/app/store/actions.ts

export enum CartActionTypes {  
    ADD_PRODUCT = 'ADD_PRODUCT',
    REMOVE_PRODUCT = 'REMOVE_PRODUCT'
}

NB: You will need to create the actions file first: touch src/app/store/actions.ts.

Now, define the actions by implemeting the Action interface:

// src/app/store/actions.ts

import { Action } from '@ngrx/store'

...
export class AddProduct implements Action {  
    readonly type = CartActionTypes.ADD_PRODUCT
    constructor(public payload: any){}
}

export class RemoveProduct implements Action {  
    readonly type = CartActionTypes.REMOVE_PRODUCT
    constructor(public payload: any){}
}

The actions AddProduct, RemoveProduct implements the Action interface, and because we will needing a product to add or remove we added a constructor to take the payload param. So that we can pass in the product when instantiating the object using the new keyword:

new Cart.AddProduct(product)  

Lastly, we will define a type alias for all actions defined above in order for it to be used in our reducer function:

// src/app/store/actions.ts

...
export type CartActions = AddProduct | RemoveProduct  

Here is the complete actions code:

// src/app/store/actions.ts

import { Action } from '@ngrx/store'

export enum CartActionTypes {  
    ADD_PRODUCT = 'ADD_PRODUCT',
    REMOVE_PRODUCT = 'REMOVE_PRODUCT'
}

export class AddProduct implements Action {  
    readonly type = CartActionTypes.ADD_PRODUCT
    constructor(public payload: any){}
}

export class RemoveProduct implements Action {  
    readonly type = CartActionTypes.REMOVE_PRODUCT
    constructor(public payload: any){}
}

export type CartActions = AddProduct | RemoveProduct  

Setting up Components

I think we're now done with setting up our store! Now, let's see how to utilize them in our components.

We have already created all the components we will be needing in our app.

products.component.ts

This will display a list of products. For this article, we are going to hard-code our list of products. You can expand this app to load the products from a resource, but we'll leave that up to you.

To hard-code our list of products, we are going to create a file that holds our products list. We will create a market.ts file:

$ touch src/app/store/market.ts

Next we will initialize an array variable PRODUCTS of type Product:

// src/app/store/market.ts

import { Product } from "./product.model";

export const PRODUCTS: Product[] = [  
    {
      id: 0,
      name: "HP Inspirion",
      price: 700
    },
    {
      id: 1,
      name: "MacBook Pro 2018",
      price: 15000
    },
    {
      id: 2,
      name: "Dell 5500",
      price: 3000
    }
]

We can now import the PRODUCTS anywhere we need it.

To display the list of products in the products.component file we are going to import PRODUCTS.

// src/app/products/products.component.ts

import { PRODUCTS } from "./../store/market";  
...

Next, we will assign it to a products variable:

// src/app/products/products.component.ts

...
export class ProductsComponent implements OnInit {

  products = PRODUCTS

  constructor() { }

  ngOnInit() { }
}

We will craft a nice HTML to display our products list:

// src/app/products/products.component.ts

...
@Component({
  selector: 'app-products',
  template: `
        <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
            <div class="my-list">
                <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                <h3>{{product.name}}</h3>
                <span>$</span>
                <span class="pull-right">{{product.price}}</span>
                <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                <div class="detail">
                    <p>{{product.name}} </p>
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                </div>
            </div>    
  `,
  styles: [ ]
})
...

We used the *ngFor directive to iterate through the products array and display it in the HTML using expression binding.

Bringing it all together, the src/app/products/products.componenet.ts, will look like this:

// src/app/products/products.componenet.ts

import { Component, OnInit } from '@angular/core';  
import { PRODUCTS } from "./../store/market";

@Component({
  selector: 'app-products',
  template: `
        <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
            <div class="my-list">
                <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                <h3>{{product.name}}</h3>
                <span>$</span>
                <span class="pull-right">{{product.price}}</span>
                <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                <div class="detail">
                    <p>{{product.name}} </p>
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                </div>
            </div>    
  `,
  styles: [ ]
})

export class ProductsComponent implements OnInit {

  products = PRODUCTS

  constructor() { }

  ngOnInit() { }
}

Next, open "src/app/styles.scss" and add the following SCSS code:

// src/app/styles.scss
...
img {  
    max-width: 100%;
}

img {  
    transition: all .5s ease;
    -moz-transition: all .5s ease;
    -webkit-transition: all .5s ease
}

.my-list {
    width: 100%;
    padding: 10px;
    border: 1px solid #f5efef;
    float: left;
    margin: 15px 0;
    border-radius: 5px;
    box-shadow: 2px 3px 0px #e4d8d8;
    position: relative;
    overflow: hidden;
}

.my-list h3 {
    text-align: left;
    font-size: 14px;
    font-weight: 500;
    line-height: 21px;
    margin: 0px;
    padding: 0px;
    border-bottom: 1px solid #ccc4c4;
    margin-bottom: 5px;
    padding-bottom: 5px;
}

.my-list span {
    float: left;
    font-weight: bold;
}

.my-list span:last-child {
    float: right;
}

.my-list .offer {
    width: 100%;
    float: left;
    margin: 5px 0;
    border-top: 1px solid #ccc4c4;
    margin-top: 5px;
    padding-top: 5px;
    color: #afadad;
}

.detail {
    position: absolute;
    top: -100%;
    left: 0;
    text-align: center;
    background: #fff;
    height: 100%;
    width: 100%;
}

.my-list:hover .detail {
    top: 0;
}

Doing this will make the SCSS code available to all our components.

App list products

product.component.ts

Here we can view a product, see its price, name, then add to cart if it appeals to us.

// src/app/product/product.component.ts

import { Component, OnInit } from '@angular/core';  
import { PRODUCTS } from "./../store/market";  
import { Product } from "./../store/product.model"  
import { ActivatedRoute } from "@angular/router";  
import { Store } from "@ngrx/store";  
import * as Cart from "./../store/actions";

@Component({
  selector: 'app-product',
  template: 
  `
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
      <div class="my-list">
          <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
          <h3>{{product.name}}</h3>
          <span>$</span>
          <span class="pull-right">{{product.price}}</span>
          <div class="offer">
            Extra 5% Off. Cart value $ {{0.5 * product.price}}
          </div>
          <div class="offer">
            <a (click)="addToCart(product)" class="btn btn-info">Add To Cart</a>
          </div>
      </div>
    </div>
  `,
  styles: [ ]
})

export class ProductComponent implements OnInit {

  product:Product

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

  ngOnInit() {
    this.route.params.subscribe((p)=>{
        let id = p['id']
        let result = Array.prototype.filter.call(PRODUCTS,(v)=>v.id == id)
        if (result.length > 0) {
          this.product = result[0]
        }
    })
  }

  addToCart(product) {
        this.store.dispatch(new Cart.AddProduct(product))
  }
}

Here we have imported ActivatedRoute so as to get the id params. It subscribes to the route events stream and then filters through the PRODUCTS array to get the matching param id in the array.

We imported the Store to dispatch ADD_PRODUCT action when the addToCart method is executed.

App view product

cart.component.ts

This displays products in our cart. Here is the code:

// src/app/cart/cart.component.ts

import { Component, OnInit } from '@angular/core';  
import { Store, select } from "@ngrx/store";  
import { Observable } from "rxjs/Observable";  
import * as Cart from "./../store/actions";

@Component({
  selector: 'app-cart',
  template: 
  `
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" *ngFor="let product of cart | async">
      <div class="my-list">
          <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
          <h3>{{product.name}}</h3>
          <span>$</span>
          <span class="pull-right">{{product.price}}</span>
          <div class="offer">
            Extra 5% Off. Cart value $ {{0.5 * product.price}}
            <a (click)="removeFromCart(product)" class="btn btn-info">Remove From Cart</a>
          </div>
      </div>
    </div>
  `,
  styles: []
})

export class CartComponent implements OnInit {

  cart: Observable<Array<any>>
  constructor(private store:Store<any>) { 
    this.cart = this.store.select('cart')
  }

  ngOnInit() { }

  removeFromCart(product) {
    this.store.dispatch(new Cart.RemoveProduct(product))
  }
}

We declared a cart variable of type Observable. Then, we select-ed the cart state from the store. The select method returns an Observable which we assign to the previously declared cart variable. The cart value is subscribed to and received using the AsyncPipe |. There is the removeFromCart method that dispatches the REMOVE_PRODUCT action to the store.

App cart

app.component.ts

This is our final component:

// src/app/app.component.ts

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

@Component({
  selector: 'app-root',
  template: `
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <h1 class="text-center">Online Store</h1>
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
        <hr />
      </div>
    </div>
    <router-outlet></router-outlet>
  </div>
  `,
  styles: []
})

export class AppComponent {  
  title = 'app';
  constructor(private store: Store<any>) {}

  cart: Observable<Array<any>>

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.cart = this.store.select('cart')
  }
}

You see we have modified the template to include the name of our app "Online Store". We subscribed to the cart store to get the number of products, then it displayed using the {{(cart | async).length}} expression. It updates real-time when we add or remove a product from the cart store.

You see the power of RxJs in play here in the one-way data flow.

To see everything we have done play out, make sure you save every file and run the following command in your terminal:

$ ng serve

Voila!!! Now you can play with the app.

Utilizing the AsyncPipe

AsyncPipe is a built-in pipe that we can use within our templates for unwrapping data from Promises or Observables.

The async pipe, when used in a component, marks it to be checked for changes.

Looking at the component where we did this:

// src/app/app.component.ts

...
@Component({
  selector: 'app-root',
  template: `
...
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
...
  `,
  ...
})
...

We could have used the subscribe method to achieve the same result:

// src/app/app.component.ts

...
@Component({
  selector: 'app-root',
  template: `
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <h1 class="text-center">Online Store</h1>
        <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{cart.length}}</a></h6>
        <hr />
      </div>
    </div>
    <router-outlet></router-outlet>
  </div>
  `,
  styles: []
})

export class AppComponent {  
  title = 'app';
  constructor(private store: Store<any>) {}

  cart: Array<any>

  ngOnInit() {
    // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    // Add 'implements OnInit' to the class.
    this.cart = this.store.select('cart')
        .subscribe(state => this.cart = state)
  }
}

You can see our code became a bit longer, but it still worked!

So, the "async pipe" does the subscription for us and appends the value to our template. So much work in only a few lines of code. Perfect!

Debugging with Redux-DevTools

Redux-Devtools is a "time-travel" debug tool for testing UI states. It makes app development and increases your development productivity.

With these tools, you can literally move into the future or the past state of your app (hence the "time travel" description).

The "locking" and "pausing" features of Redux-DevTools makes it possible to remove past actions from history or disable them.

We can use Redux-Devtools in an Angular app, but the ngrx team developed its own dev-tool for use in any ngrx-powered app.

It can be installed by running the following command:

$ npm i @ngrx/store-devtools -S

Here is the link to download the Redux-DevTools Extension.

Import StoreDevtoolsModule.instrumentOnlyWithExtension() in your app.module.ts:

import { StoreDevtoolsModule } from '@ngrx/store-devtools'

@NgModule({
    imports: [
        StoreDevtoolsModule.instrumentOnlyWithExtension({
            maxAge: 6
        })
    ]
})

export AppModule() {}  

We won't go into technical details on how to utilize Redux-DevTools. Instead, you can take it up as a task to improve yourself by adding it to this project, I'll like to hear from you! Feel free to send me a PR!

Resources

Want to learn more about Angular, TypeScript, and other useful front-end libraries? I'd recommend checking out some more detailed resources, like an online course:

Conclusion

I know, we violated several best practices, but most importantly you have seen how to use @ngrx/store to build Angular applications.

If you got lost or need a reference to the code, you can fork it or clone it from my GitHub repository. You can expand the application to do things I didn't think of. Feel free, play around with it.

Author image
I am Chidume Nnamdi, a software dev and tech. writer from Nigeria.