Adding a component – introducing selectors

That's it, thats all you need to create an effect. We're not done here though, we need a component to display our data, as well as a spinner, while we are waiting for the AJAX request to finish.

Ok, first things first: what do we know of components that should be using NgRx? The obvious answer is that they should be injecting the store so we can listen to a slice of state from the store. The way we listen to a slice of state is by calling the stores select() function. This will return an Observable. We know we can easily show Observables in the template through the use of the async pipe. So let's start sketching our component:

// product/product.component.ts

import { Component, OnInit } from "@angular/core";
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";


@Component({
selector: "products",
template: `
<div *ngFor="let product of products$ | async">
Product: {{ product.name }}
</div>
</div>
`
})
export class ProductsComponent {
products$;
loading$;

constructor(private store: Store<AppState>) {
this.products$ = this.store.select((state) => {
return state.products.list;
});

}
}

This part of our component here shouldn't come as too much of a surprise; we inject the store into the constructor, call select(), and get an Observable back. But, there is a but here, we are calling the select() method differently. We used to pass a string to the select() function, and now we pass it a function. Why is that? Well, because we changed how our state looked. Let's show our new state again, for clarity:

const initialState = {
loading: false,
list: [],
error: void 0
}

The preceding code shows that we can't just do store.select("products") because that would give us the whole object back. So we need a way to dig into the previous object in order to get a hold of the list property that should contain our list of products. The way to do that is to use the variant of the select method that takes a function instead. We do just that with the following code:

this.products$ = this.store.select((state) => {
return state.products.list;
});

Ok, but will this really be type safe? Won't the AppState interface complain? Does it know of our changed state structure? Well, we can tell it knows about it, but we need to ensure that our reducer exports an interface that represents our new state structure. We therefore change the reducer to look like the following:

// product/products-reducer.ts

import {
FETCHING_PRODUCTS_SUCCESSFULLY,
FETCHING_PRODUCTS_ERROR,
FETCHING_PRODUCTS
} from "./product-constants";

export interface ProductsState {
loading: boolean;
list: Array<Product>;
error: string;
}

const initialState: ProductsState = {
loading: false,
list: [],
error: void 0
}

export function productReducer(state = initialState, action) {
switch(action.type) {
case FETCHING_PRODUCTS_SUCCESSFULLY:
return { ...state, list: action.payload, loading: false };
case FETCHING_PRODUCTS_ERROR:
return { ...state, error: action.payload, loading: false };
case FETCHING_PRODUCTS:
return { ...state, loading: true };
default:
return state;
}
}

And of course, we need to update the AppState interface to look like this:

// app-state.ts

import { FeatureProducts } from "./product/product.reducer";

export interface AppState {
featureProducts: FeatureProducts;
}

Ok, this made our AppState know what kind of beast our products property really is, and is thereby what makes the store.select(<Fn>) call possible. The function we gave the select method is called a selector, and is actually something that doesn't have to live inside the component. The reason for this is that we might want to access that slice of state somewhere else. Let's therefore create a product.selectors.ts file. We will add to this later as we keep supporting CRUD:

// product/product.selectors.ts
import { AppState } from "../app-state";

export const getList = (state:AppState) => state.featureProducts.products.list;
export const getError = (state:AppState) => state.featureProducts.products.error;
export const isLoading = (state:AppState) => state.featureProducts.products.loading;

Ok, so now we have created our selectors file, and we can immediately start improving our components code and clean it up a bit before we continue to add things to it:

// product/product.component.ts

import { Component, OnInit } from "@angular/core";
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { getList } from './product.selectors';

@Component({
selector: "products",
template: `
<div *ngFor="let product of products$ | async">
Product: {{ product.name }}
</div>
`
})
export class ProductsComponent {
products$;

constructor(private store: Store<AppState>) {
this.products$ = this.store.select(getList);
}
}

Our code looks much better. It's time to start caring about the other aspect of this; what if our HTTP service takes a few seconds, or even one second to return? This is a real concern especially with our users being potentially on a 3G connection. To take care of this, we grab the loading property from our products state and  use that as a conditional in our template. We will basically say that if the HTTP call is still pending, show some text or an image that indicates to the user that something is loading. Let's add that piece of functionality to the component:

import { Component, OnInit } from "@angular/core";
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { getList, isLoading } from "./products.selectors";

@Component({
selector: "products",
template: `
<div *ngFor="let product of products$ | async">
Product: {{ product.name }}
</div>
<div *ngIf="loading$ | async; let loading">
<div *ngIf="loading">
loading...

</div>
</div
>
`
})
export class ProductsComponent {
products$;
loading$;

constructor(private store: Store<AppState>) {
this.products$ = this.store.select(getList);
this.loading$ = this.store.select(isLoading);
}
}

Let's also ensure that we show any errors by subscribing to products.error. We simply update the component with the following alterations:

import { Component, OnInit } from '@angular/core';
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { getList, isLoading, getError } from "./products.selectors";

@Component({
selector: "products",
template: `
<div *ngFor="let product of products$ | async">
Product: {{ product.name }}
</div>
<div *ngIf="loading$ | async; let loading">
<div *ngIf="loading">
loading...
</div>
</div>
<div *ngIf="error$ | async; let error" >
<div *ngIf="error">{{ error }}</div>
</div>
`
})
export class ProductsComponent {
products$;
loading$;
error$;

constructor(private store: Store<AppState>) {
this.products$ = this.store.select(getList);
this.loading$ = this.store.select(isLoading);
this.error$ = this.store.select(getError);
}
}

Ok, we fire up our application at this point. There is just one teeny tiny problem; we don't see any products at all. Why is that? The explanation is simple. We don't actually dispatch an action that will lead to the AJAX call being made. Let's fix that by adding the following code to our component:

import { Component, OnInit } from '@angular/core';
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { getList, isLoading, getError } from "./products.selectors";
import { fetchProducts } from "./products.actions";

@Component({
selector: "products",
template: `
<div *ngFor="let product of products$ | async">
Product: {{ product.name }}
</div>
<div *ngIf="loading$ | async; let loading">
<div *ngIf="loading">
loading...
</div>
</div>
<div *ngIf="error$ | async; let error" >
<div *ngIf="error">{{ error }}</div>
</div>
`
})
export class ProductsComponent implements OnInit {
products$;
loading$;
error$;

constructor(private store: Store<AppState>) {
this.products$ = this.store.select(getList);
this.loading$ this.store.select(isLoading);
this.error$ = this.store.select(getError);
}

ngOnInit() {
this.store.dispatch(fetchProducts);
}
}

This will of course trigger our effect, which will lead to our HTTP call, which will lead to fetchProductsSuccessfully() being called, and thereby our state will be updated and products.list will no longer be an empty array, meaning our UI will show a list of products. Success!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.99.106