• NgRx


    Category

    Introduction

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

    • The biggest problem in the development and maintenance of large-scale software system is complexity - large system are hard to understand.
    • Reactive programming is when we react to data being streamed to us over time.

    Source Code

     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 aps. 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 Reduc that was developed with Rxjs while keeping the core concepts and API of Redux
      
    3. ngrx, being inspired by Redux, shares the same principles with it and supercharging it with RxJS.

    What is RxJs

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

    Talking about streams -a stream is a sequence of values in time. This streams of events and data in real-time- which we call Observable S - 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));
    

    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 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 store. The states are updated using actions transported to pure functions called reducers. Reducer take in state and action as parameters and perform an immutable action on the state and return 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 comprise of different states defined in our application. The state, thus, is immutable and only altered by action.

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

    The store combines the whole application state into a single , 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 event.

    export function reducer(state=initialState,action:articles.Action):State{
    	switch(action.type){
            case 'ADD-ARTICLE':
                return {
                    articles:[...state.articles,action.payload]
                }
            default:
                return state;
        }
    }
    

    State

    State is a single immutable data structure. State 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 store, but how do we update the store when the need arises? That is the role of actions. Actions represent payloads of information that are dispatched to the store from the application and are usually triggered by user interaction.

    export interface Action{
    	type:string,
    	payload?:any
    }
    
    dispatch({type:'ADD_ARTICLE', payload:{link:'github.com/baron',points:90}})
    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 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, Performance, and DevTools.

    • Centralized State: State in a store is kept in one directory. It makes it easier to foretell updates of 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 "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 I got to see how the library was brilliantly built. In doing so, I really got acquainted with stuff.

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

    • Store is where it all states, it instantiates other classes. It extends the Observable class so that we can subscribe to it get the latest state.
    • ActionSubject 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 ActionSubject 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 wen can use it from any directory in our system.

    Setup

    We are now set. We will call our 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
    

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

    @import "~bootstrap/scss/bootstrap.scss"
    
    npm install @ngrx/store @ngrx/core -S
    

    Our app will have three components:

    • products.components: That will display lists of products and their prices.
    • cart.component: This component displays all item 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 --sepc=false
    ng g c cart --inline-style=true --sepc=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 skip 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 a particular product.
    • /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 RouteModule 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 out 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 rotes match the use'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 al 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. Immutability is the key here.

    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 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 fromsrc/app/store/reducer.tsfile. 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 action 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 function. 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 actins file first: touch src/app/store/acions.ts.

    Now define the actions by implementing 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 keywork:

    new Cart.AddProduct(product)
    

    Lastly, we will define a type alias for all actions defined above in order for t to be used 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 pur app.

    products.components.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.component.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 "sec/app/styles.scss" and add the following scss code:

    // src/app/styles.scss
    ...
    img {
        max- 100%;
    }
    
    img {
        transition: all .5s ease;
        -moz-transition: all .5s ease;
        -webkit-transition: all .5s ease
    }
    
    .my-list {
         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 {
         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%;
         100%;
    }
    
    .my-list:hover .detail {
        top: 0;
    }
    

    product.component.ts

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

    // 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 ActivateRoute 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.

    cart.component.ts

    This display 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. Then cart value is subscribed to and received using the AsyncPipe|. There is the removeFromCart method that dispatch the REMOVE_PRODUCT action to the store.

    app.coponent.ts

    // 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 display 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
    

    Utilizing the AsyncPipe

    AsyncPipe is a built-in pipe that we can use within our templates for unwrapping data from Promise or Observable

    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.

    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 devtool for use in any ngrx -powered app.

    It can be installed by running the following command:

    npm i @ngrx/store-devtools -S
    

    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.

    Resources

    Conclusion

  • 相关阅读:
    20199108 2019-2020-2 《网络攻防实践》第8周作业
    20199108 2019-2020-2 《网络攻防实践》第7周作业
    攻防作业报告提交
    Spring整合HBase
    基于注解的Spring AOP示例
    Spring AOP基本概念
    在RichFaces中使用Facelets模板
    算法导论读书笔记(19)
    算法导论读书笔记(18)
    算法导论读书笔记(17)
  • 原文地址:https://www.cnblogs.com/baron-li/p/15183919.html
Copyright © 2020-2023  润新知