This chapter covers
You’ve made it to the last chapter, and you’re almost ready to join an Angular project. The previous chapter was easy reading, but this chapter will require your full attention; the material we’re about to present has many moving parts, and you’ll need to have a good understanding of how they play together.
ngrx is a library that can be used for managing state in Angular apps (see https://github.com/ngrx). It’s built using the principles of Redux (another popular library for managing state), but the notification layer is implemented using RxJS. Although Angular has other means for managing app state, ngrx is gaining traction in mid- and large-size apps.
Is it worth using the ngrx library for managing state in your app? It certainly has benefits, but they don’t come free. The complexity of your app can increase, and the code will become more difficult to understand by any new person who joins the project. In this chapter, we cover the ngrx library so you’ll be able to decide whether it’s the right choice for managing the state of your app. In the hands-on section, we do a detailed code overview of yet another version of ngAuction that uses ngrx for state management.
Imagine that you’re a proud owner of a convenience store that sells various products. Remember how you started? You rented an empty place (the store was in its initial state). Then you purchased shelves and ordered products. After that, multiple vendors started delivering those products. You hired employees who arranged these products on the shelves in a certain order, changing the state of the store. Then you put out the Grand Opening sign and festooned the place with lots of colorful balloons. Customers started visiting your store to buy products.
When the store is open, some products lay on the shelves, and some are in shopping carts of customers. Some customers are waiting in lines at cash registers, where there are store employees. You can say that at any given moment, your store has the current state.
If a customer takes an action, such as buying five bottles of water, the cashier scans the barcode, and this reduces the number of bottles in the inventory—it updates the state. If a vendor delivers new products, your clerk updates the inventory (state) accordingly.
Your web app can also maintain a store that holds the state of your app. Like a real store, at any given time your app’s store has a current state. Some data collections have specific data retrieved from the server and possibly modified by a user. Some radio buttons are checked, and a user selects some products and navigates to some routes represented by a specific URL.
If a user interacts with the UI, or the server sends new data, these actions should ask the store object to update the state. To keep track of state changes, the current state object is never updated, but a new instance of the state object is created.
Redux is an open source JavaScript library that offers a state container for JavaScript apps (see http://mng.bz/005X). It was created at Facebook as an implementation of the Flux architecture (see http://mng.bz/jrXy). Initially, the developers working with the React framework made Redux popular, but as it’s a JavaScript library, it can be used in any JavaScript app.
Redux is based on the following three principles:
In Redux, the data flow is unidirectional:
1. The app component dispatches the action on the store.
2. The reducer (a pure function) takes the current state object and then clones, updates, and returns it.
3. The app component subscribes to the store, receives the new state object, and updates the UI accordingly.
Figure 15.1 shows the unidirectional Redux data flow.
An action is a JavaScript object that has a type property describing what happens in your app, such as a user wants to buy IBM stock. Besides the type property, an action object can optionally have another property with a payload of data that should change the app state in some fashion. An example is shown in the following listing.
{ type: 'BUY_STOCK', 1 stock: {symbol: 'IBM', quantity: 100} 2 }
This object only describes the action and provides the payload. It doesn’t know how the state should be changed. Who does? The reducer.
A reducer is a pure function that specifies how the state should be changed. The reducer never changes the current state, but creates a new (and updated) version of it. The state object is immutable. The reducer creates a copy of the state object and returns a new reference to it. From an Angular perspective, it’s a binding change event, and all interested parties will immediately know that the state has changed without requiring expensive value checking in the entire state tree.
Your state object can contain dozens of properties and nested objects. Cloning the state object creates a shallow copy without copying each unmodified state property in memory, so memory consumption is minimal and it doesn’t take much time. You can read about the rationale for creating shallow state copies at http://mng.bz/3271.
A reducer function has the signature shown in the following listing.
function (previousState, action): State {...} 1
Should the reducer function implement app functionality like placing an order, which requires work with external services? No, because reducers are meant for updating and returning the app state—for example, the stock to buy is "IBM". Implementing app logic would require interaction with the environment external to the reducer; it would cause side effects, and pure functions can’t have side effects.
The reducer can implement minimal app logic related to state change. For example, suppose a user decides to cancel an order, which requires a reset of certain fields on the state object. The main app logic remains in your application code (for example, in services) unless a concrete implementation of the Redux-inspired library offers a special place meant for code with side effects. In this chapter, we use the ngrx library, which suggests using Angular services combined with so-called effects that live outside of the store and that can aggregate Angular services working as a bridge between the store and services.
Recently, one of the authors of this book worked on a web project for a large car manufacturer. This was a web app that allowed a prospective buyer to configure a car by selecting from more than a thousand packages and options (such as model, interior and exterior colors, length of chassis, and so on). The app was developed over many years. The software modules were written using JavaScript, jQuery, Angular, React, and Handlebars, as well as the HTML templating engine Thymeleaf on the server.
From a user perspective, this was one workflow that consisted of several steps resulting in configuring and pricing the car based on selected options. But internally, the process was switching from one module to another, and each module needed to know what was selected in the previous step to show the available options.
In other words, each module needed to know the current state of the app. Depending on the software used in any particular module, the current user selections were stored using one of the following:
New requirements came in, new JIRA tickets were created and assigned, and implementation would begin. Time and time again, implementing a seemingly simple new requirement would turn into a time-consuming and expensive task. Good luck explaining to the manager why showing the price in page B would take a half day even though this price was already known in page A, or that the state object used in page B didn’t expect to have the price property, and if in page A the price was a part of the URL, page B expected to get the current state from local storage. Rewriting it from scratch was not an option. It would have been so much easier if app state had been implemented in a uniform way and stored in a single place!
If you’re starting to develop a new project, pay special attention on how app state is implemented, and it will help you greatly in the long run.
ngrx is a library inspired by Redux. You can think of it as an implementation of the Redux pattern for managing app state in Angular apps. Similarly to Redux, it implements a unidirectional data flow and has a store, actions, and reducers. It also uses RxJS’s ability to send notifications and subscribe to them.
Large enterprise apps often implement messaging architecture on the server side, where one piece of software sends a message to another via some messaging server or a message bus. You can think of ngrx as a client-side messaging system. The user clicks a button, and the app sends a message (for example, dispatches an action). The app state changed because of this button click, and the ngrx Store sends a message to the subscriber(s), emitting the next value into an observable stream.
In section 15.1, we described three Redux principles:
In ngrx, app state is accessed with the Store service, which is an observable of state and an observer of actions. The declaration of the class Store in the store.d.ts file looks like this:
class Store<T> extends Observable<T> implements Observer<Action>
Besides declaring a new principle, the ngrx architecture includes effects, which are meant for the code that communicates with other parts of the app, such as making HTTP requests. With ngrx selectors, you can subscribe to changes in a particular branch of the state object. There’s also support for routing and collections of entities, which can be useful in CRUD operations.
We’ll start our ngrx introduction with its main players: a store, actions, and reducers.
Let’s see how to use ngrx in a simple app that has two buttons that can either increment or decrement the value of the counter. The first version of this app doesn’t manage state and looks like the following listing.
import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: ` <button (click)="increment()">Increment</button> <button (click)="decrement()">Decrement</button> <p>The counter: {{counter}}</p> 1 ` }) export class AppComponent { counter = 0; increment() { this.counter++; 2 } decrement() { this.counter--; 3 } }
You want to change this app so that the ngrx store manages the state of the counter variable, but first you need to install the ngrx store in your project:
npm i @ngrx/store
The Store serves as a container of the state, and dispatching actions is the only way to update the state. The plan is to instantiate the Store object and remove the app logic (incrementing and decrementing the counter) from the component. Your decrement() and increment() methods will be dispatching actions on the Store instead.
Actions are handled by the ngrx reducer, which will update the state of the counter. The type of your counter variable will change from number to Observable, and to get and render its emitted values in the UI, you’ll subscribe to the Store.
The only required property in the Action object is type, and for your app, you’ll declare the action types as follows:
const INCREMENT = 'INCREMENT'; const DECREMENT = 'DECREMENT';
The next step is to create a reducer function for each piece of data you want to keep in the store. In your case, it’s just the value of the counter, so you’ll create a reducer with the switch statement for updating state based on the received action type, as shown in the following listing. Remember, the reducer function takes two arguments: state and action.
import { Action } from '@ngrx/store'; export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export function counterReducer(state = 0, action: Action) { 1 switch (action.type) { 2 case INCREMENT: return state + 1; 3 case DECREMENT: return state - 1; 4 default: return state; 5 } }
It’s important to note that the reducer function doesn’t modify the provided state, but returns a new value. The state remains immutable.
Now you need to inform the root module that you’re going to use the counterReducer() function as a reducer for your store, as shown in the following listing.
import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; import {AppComponent} from './app.component'; import {counterReducer} from "./reducer"; import {StoreModule} from "@ngrx/store"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, StoreModule.forRoot({counterState: counterReducer}) 1 ], providers: [], bootstrap: [AppComponent] }) export class AppModule {}
In this code, you configure the app-level store to provide the object that specifies counterReducer as the name of the reducer function, and counterState as the property where this reducer should keep the state.
Finally, you need to change the code of your component to dispatch either the action of type INCREMENT or DECREMENT, depending on which button a user clicks. You’ll also inject the Store into your component and subscribe to its observable that will emit a value each time the counter changes, as shown in the following listing.
import {Component} from '@angular/core'; import {Observable} from "rxjs"; import {select, Store} from "@ngrx/store"; import {INCREMENT, DECREMENT} from "./reducer"; @Component({ selector: 'app-root', template: ` <button (click)="increment()">Increment</button> <button (click)="decrement()">Decrement</button> <p>The counter: {{counter$ | async}}</p> 1 ` }) export class AppComponent { counter$: Observable<number>; 2 constructor(private store: Store<any>) { this.counter$ = store.select('counterState'); 3 } increment() { this.store.dispatch({type: INCREMENT}); 4 } decrement() { this.store.dispatch({type: DECREMENT}); 5 } }
Note that an action is an object (for example, {type: INCREMENT}), and in this app, action objects have no payload. You can also think of an action as a message or a command. In the next section, you’ll be defining each action as a class with two properties: type and payload.
In this component, you use the select operator (defined in the ngrx Store), which allows you to observe the state object. The name of its argument must match the name of the state object property used in the StoreModule.forRoot() function.
The counterReducer was assigned to the store by invoking the method StoreModule.forRoot({counterState: counterReducer}) in the module. The AppComponent communicated with the counterReducer either by dispatching an action on the store or by using the select operator on the store.
The app with the ngrx store will have the same behavior as the original one and will increment and decrement the counter depending on the user’s action.
Now let’s check whether your store is really a single source of truth. You’ll add a child component in the next listing that will display the current value of the counter received from the store, and 10 seconds after app launch, the child will dispatch the INCREMENT action.
import {Component} from '@angular/core'; import {select, Store} from "@ngrx/store"; import {Observable} from "rxjs"; import {INCREMENT} from "../reducer"; @Component({ selector: 'app-child', template: ` <h3> Child component </h3> <p> The counter in child is {{childCounter$ | async}} </p> `, styles: [] }) export class ChildComponent { childCounter$: Observable<number>; constructor(private store: Store<any>) { 1 this.childCounter$ = store.pipe(select('counterState')); 2 setTimeout(() => this.store.dispatch({type: INCREMENT}), 3 10000); } }
The only thing left is adding the <app-child> tag to the template of AppComponent. Figure 15.2 shows the app after the user clicks the Increment button three times. Both parent and child components show the same value of the counter taken from the store (single source of truth). Ten seconds after the app starts, the ChildComponent dispatches the INCREMENT action, and both components will show the incremented counter.
To see this app in action, open the project counter, run npm install, and then run ng serve -o.
The ngrx library includes the example app (see http://mng.bz/7F9x), which allows you to maintain a book collection using Google Books API. The ngAuction app that comes with this chapter can also serve as an ngrx demo, although neither of these apps uses every API offered by ngrx.
The counter app is a pretty basic example, with a single reducer function. In practice, a store may have several reducers where each of them would be responsible for a portion of the state object. In the hands-on section, the new version of ngAuction will have several reducers.
Here’s a diagram you saw in chapter 8 in section 8.3.1.
A view consists of components
Say component 7 can emit some data that’s needed in component 6. If you use the common parents for intercomponent communication, you need to emit an event via the @Output property of component 7; parent component 3 would subscribe to this event and reemit it through its @Output property; and component 1 would subscribe to this event and, via binding, pass the payload to component 6.
Using the ngrx store eliminates the need to program this series of unfortunate events. Component 7 emits an action of the store, and component 6 uses a selector to receive it. The same, simple model of intercomponent communication works regardless of how many levels of component nesting exist in any particular view. The only thing that both components 7 and 6 need is the reference to the store object.
This figure doesn’t give any details about what these eight components do, but you can assume that 1, 2, and 3 are container components, which include other components and implement app logic for interacting with their children, parents, and services. The rest are presentational components that can get data in, send data out, and present data on the UI. Some authors suggest that only container components should manage state and communicate with the store. We don’t agree with this, because state is not only about storing and sharing data—it’s also about storing the state of the UI, which is a part of any type of component.
In the counter example, the store managed app state represented by a number, but did you notice the store played yet another role? In section 8.3.2 in chapter 8, we showed you how an injectable service can play the role of mediator. In the counter app, the main goal of the ngrx store is to manage app state, but it also plays another role: serving as a mediator between parent and child components.
In chapter 8, the mediator was a service with an RxJS BehaviorSubject, and you used components for sending and receiving data. With ngrx, you don’t need to manually create BehaviorSubject, because the Store object can be used for emitting values as well as subscribing to them.
To notify BehaviorSubject about the new value, you use next(), and to notify the store about the new state, you use dispatch(). To get the new state, subscribe to the observable in both cases. Figure 15.3 compares the code of EbayComponent from listing 8.13 in chapter 8 (on the left) with ChildComponent that uses ngrx (on the right). They look similar, don’t they?
We can say that the StateService (left) and the Store (right) each serve as a single source of truth. But large non-ngrx apps with multiple injectable services that store different slices of state would have multiple sources of truth. In ngrx apps, the Store service always remains a single source of truth, which may have multiple slices of state.
Now take another look at the reducer in listing 15.4, which was a pure function that didn’t need to use any external resource to update the state. What if the value for a counter was provided by a server? The reducer can use external resources because it would make the reducer impure, wouldn’t it? This is where ngrx effects come in, and we’ll discuss them next.
Reducers are pure functions that perform simple operations: take the state and action and create a new state. But you need to implement business logic somewhere, such as reaching out to services, making requests to servers, and so on. You need to implement functions with side effects, which is done in effect classes.
Effects are injectable classes that live outside of the store and are used for implementing functionality that has side effects, without breaking unidirectional data flow. ngrx effects come in a separate package, and you need to run the following command to add them to your project:
npm i @ngrx/effects
If a component dispatches an action that requires communication with external resources, the action can be picked up by the Effects object, which will handle this action and dispatch another one on the reducer. For example, an effect can receive a LOAD_PRODUCTS action from the store, invoke loadProducts(), and, when the data is loaded, dispatch either of the LOAD_PRODUCTS_SUCCESS or LOAD_PRODUCTS_FAILURE actions. The reducer will pick it up and update state accordingly. Figure 15.4 shows the ngrx flow that uses effects.
To understand this diagram, imagine that a user clicks the Buy 100 button. The component would dispatch an action on the store, which can be handled by a reducer, an effect, or both. An effect can access external services and dispatch another action. In any case, a reducer is ultimately responsible for creating a new state, and a component can get it using a selector and update the UI accordingly (such as by rendering the message “Buying 100 shares”).
We’d like to stress that even though actions can be handled in both a reducer and an effect, only a reducer can change the state of an app.
If you compare the Redux and ngrx data flows shown in figures 15.4 and 15.1 respectively, you’ll notice that the effects live outside the store. They can communicate with other Angular services, which in turn can communicate with external servers, if need be. Another difference in figure 15.1 is that the view would use subscribe() to receive the latest state; 15.4 shows the select() method that can use a selector function to retrieve either the entire state object or its part.
In both Redux and ngrx, a component dispatches actions on the store. Redux actions are handled only in reducers, but in ngrx, some actions are handled in reducers, some in effects, and some in both. For example, if a component dispatches LOAD_PRODUCTS, a reducer can pick it up to set the state property loading to true, which will result in displaying a progress indicator. An effect can receive the same LOAD_PRODUCTS action and make an HTTP request for products.
You know that to dispatch an action that should be handled by the reducer, a component invokes Store.dispatch(), but how can an effect dispatch an action? An effect returns an observable that wraps some payload. In your effects class, you’ll declare one or more class variables annotated with the @Effect decorator. Each effect will apply the ofType operator to ensure that it reacts to only the specified action type, as shown in the following listing.
@Injectable() export class MyEffects { ... @Effect() loadProducts$ = this.actions$ .pipe(ofType(LOAD_PRODUCTS), .switchMap(this.productService.getProducts())) ... }
In this example, the @Effect decorator marks the observable property loadProducts$ as a handler for the actions of type LOAD_PRODUCTS and invokes getProducts(), which returns an Observable. Then, based on the emitted value, the effect will dispatch another action (for example, success or failure). You’ll see how to do this in the next section. In general, you can think of an effect as middleware between the original action and the reducer, as shown in figure 15.5.
In your app module class, you need to add to the @NgModule decorator EffectsModule.forRoot() for the root module, or EffectsModule.forFeature() for a feature module.
We don’t want to overwhelm you with the theory behind ngrx, so let’s continue developing an app that uses an ngrx store, actions with payloads, reducers, effects, and selectors.
In this section, you’ll refactor the app created in section 8.3.2 in chapter 8. That app had a search field and two links, eBay and Amazon. You’ll refactor it by replacing the SearchService injectable that was maintaining app state with an ngrx store. To illustrate communication between effects and services, you’ll add ProductService, which will generate the search results: the products containing the entered search criterion in their names.
The new version of the app is located in the mediator folder. It will use the following ngrx building blocks:
Figure 15.6 shows the mediator app after a user enters aaa in the search field of the SearchComponent.
The state object of this app will contain two properties: search query (for example, aaa) and search results (for example, five products). You’ll declare the type in the following listing to represent the state of your app.
export interface State { searchQuery: string; 1 searchResults: string[]; 2 }
In the counter app, actions don’t contain payloads; they increment or decrement the counter. This time it’s different. The SEARCH action can have a payload (such as aaa), and SEARCH_SUCCESS can have a payload as well (such as five products). That’s why declaring constants representing the action type isn’t enough, and you’ll wrap each action into a class with a constructor that has a payload as an argument. The actions will be declared in the file actions.ts, shown in the following listing.
import {Action} from '@ngrx/store'; export const SEARCH = '[Product] search'; 1 export const SEARCH_SUCCESS = '[Product] search success'; 1 export class SearchAction implements Action { 2 readonly type = SEARCH; constructor(public payload: {searchQuery: string}) {} } export class SearchSuccessAction implements Action { 3 readonly type = SEARCH_SUCCESS; constructor(public payload: {searchResults: string[]}) {} } export type SearchActions = SearchAction | SearchSuccessAction; 4
Note the text [Product] in the action definitions. In real-world apps, you may have more than one SEARCH action—one for products, one for orders, and so on. By prepending the action description with [Product], you create a namespace to make the code more readable. Having namespaced actions helps in understanding which actions were dispatched at any given moment.
The last line of actions.ts uses the TypeScript union operator described in section B.11 in appendix B. Here, you define the SearchActions type that will be used in the reducer’s signature, so the TypeScript compiler knows which actions are allowed in the reducer’s switch statement.
The actions are declared, but someone has to create and dispatch them. In your app, the SearchComponent shown in the following listing will be creating and dispatching the action of type SEARCH after the user enters the search criterion.
@Component({ selector: 'app-search', template: ` <h2>Search component</h2> <input type="text" placeholder="Enter product" [formControl]="searchInput">`, styles: ['.main {background: yellow}'] }) export class SearchComponent { searchInput: FormControl; constructor(private store: Store<any>) { this.searchInput = new FormControl(''); this.searchInput.valueChanges 1 .pipe(debounceTime(300), tap(value => console.log(`The user entered ${value}`))) .subscribe(searchValue => { this.store.dispatch(new SearchAction({ searchQuery: searchValue }));2 }); } }
The dispatched action will be picked up by the reducer, which will update the searchQuery property on the state object.
We’ll talk about another action creator, the SearchEffects class, later in this section.
In the reducer shown in listing 15.12, you declare the interface describing the structure of your app state and create an object that represents an initial state. The reducer() function will take the initial or current immutable state and, using a switch statement, will create and return a new state based on the action type.
import {SearchActions, SEARCH, SEARCH_SUCCESS} from './actions'; export interface State { 1 searchQuery: string; searchResults: string[]; } const initialState: State = { 2 searchQuery: '', searchResults: [] }; export function reducer(state = initialState, action: SearchActions): State { switch (action.type) { case SEARCH: { 3 return { 4 ...state, searchQuery: action.payload.searchQuery, 5 searchResults: [] 5 } } case SEARCH_SUCCESS: { 6 return { ...state, 7 searchResults: action.payload.searchResults 8 } } default: { return state; 9 } } }
If, in a case clause, you use the action type that wasn’t declared in the union type SearchActions (for example, SEARCH22), the TypeScript compiler returns an error.
The more precise name for TypeScript union types is discriminated unions. If all the types in a union have a common type property, the TypeScript compiler can discriminate types by this property. It knows which particular type from the union was referred to within the case statement and suggests the correct type for the payload property.
For cloning the state object and updating some of its properties, you use the spread operator described in section A.7 in appendix A. Note that state properties will be updated with the value of the action payload.
In this app, you’ll have one effect that will use a ProductService injectable to obtain products. To simplify the explanation, you don’t load products from an external server or file. Your ProductService, shown in the following listing, will generate and return an observable of five products. It uses the RxJS delay operator to emulate a one-second delay as if the products are coming from a remote computer.
@Injectable() export class ProductService { static counter = 0; 1 getProducts(searchQuery: string): Observable<string[]> { const productGenerator = () => `Product ${searchQuery}${ProductService.counter++}`; 2 const products = Array.from({length: 5}, productGenerator); 3 return Observable.of(products).pipe(delay(1000)); 4 } }
Your SearchEffects class will declare one effect, loadProducts$, that will dispatch the SEARCH_RESULTS effect having an array of products as its payload. You want to ensure that this effect will obtain products only if the store dispatched the SEARCH effect, so you use the ngrx operator ofType(SEARCH).
This effect extracts the payload of the action of type SEARCH (the search query) emitted by the actions$ observable, and, using switchMap, will pass it over to the inner observable (the getProducts() method). Finally, the effect will dispatch the action of type SEARCH_RESULTS with the payload, all of which you can see in the following listing.
@Injectable() export class SearchEffects { @Effect() loadProducts$ = this.actions$ 1 .ofType(SEARCH) 2 .pipe( map((action: SearchAction) => action.payload), 3 switchMap(({searchQuery}) => this.productService.getProducts(searchQuery)), 4 map(searchResults => new SearchSuccessAction({searchResults})) 5 ); constructor(private actions$: Actions, 6 private productService: ProductService) {} 7 }
In this example, you assume that getProducts() will always emit products, but you could add the catchError() function to the observer, where you’d emit the action that reports an error. You’ll see the use of catchError() in listing 15.31.
Although it’s okay to abandon unwanted results with switchMap while reading data, if you write an effect that performs the add, update, or delete operations, use concatMap instead. This will prevent possible race conditions when one request is in the middle of updating a record and another one comes in. With concatMap, all requests will arrive at the service one after another.
In some cases, you may want to create an effect that handles an action but doesn’t need to dispatch another one. For example, you may want to create an effect that merely logs the action. In such cases, you need to pass a {dispatch: false} object to the @Effect decorator:
@Effect({ dispatch: false }) logAction$ = this.actions$ .pipe( tap( action => console.log(action)) );
In real-world apps, the state object can be represented by a tree of nested properties, and you may want to obtain specific slices of the store state rather than obtain the entire state object and manually traverse its content. Let’s see how app components can get the value of a specific state property by using selectors.
First, get the selector of the top-level feature state using the createFeatureSelector() method. Then, use this selector as a starting point for other more specific selectors using the createSelector() method, which returns a callback function for selecting a slice of state. The selectors of your app are declared in the file selectors.ts.
import {createFeatureSelector, createSelector} from '@ngrx/store'; import {State} from './reducers'; export const getState = createFeatureSelector<State>('myReducer'); 1 export const getSearchQuery = createSelector(getState, state => state.searchQuery); 2 export const getSearchResults = createSelector(getState, state => state.searchResults); 3
The argument of the createFeatureSelector() method is the name of the reducer specified in the module. In the @NgModule decorator, you’ll have the following line:
StoreModule.forRoot({myReducer: reducer})
That’s why, to obtain the reference to this reducer, you write createFeatureSelector ('myReducer');.
Let’s recap what you’ve accomplished so far:
1. You declared classes to represent the actions of types SEARCH and SEARCH_RESULTS.
2. The SearchComponent can dispatch the action of type SEARCH.
3. The reducer can handle both action types.
4. You declared the effect that can obtain products and dispatch the action of type SEARCH_RESULTS.
5. You declared selectors to obtain slices of the app state.
To close the loop, you’ll use the selectors in eBay and Amazon components to render the search criterion and the retrieved products. The following listing shows only the code of the EbayComponent (the code of the AmazonComponent looks identical).
@Component({ selector: 'app-ebay', template: ` <div class="ebay"> <h2>eBay component</h2> Search criteria: {{searchFor$ | async}} 1 <ul> <li *ngFor="let p of searchResults$ | async ">{{ p }}</li> 2 </ul> </div>`, styles: ['.ebay {background: cyan}'] }) export class EbayComponent { searchFor$ = this.store.select(getSearchQuery); 3 searchResults$ = this.store.select(getSearchResults); 4 constructor(private store: Store<State>) {} 5 }
The code of EBayComponent is concise and doesn’t contain any app logic. With ngrx, you need to write more code, but each method in your Angular component becomes either a command that sends an action or a selector that retrieves the data, and each command changes the state of your app.
There’s one more step to complete the app-ngrx communication. You need to register the store and effects in the app module. Your module, shown in the next listing, also includes route configuration, so a user can navigate between the eBay and Amazon components.
@NgModule({ imports: [BrowserModule, CommonModule, ReactiveFormsModule, RouterModule.forRoot([ {path: '', component: EbayComponent}, 1 {path: 'amazon', component: AmazonComponent}]), 1 StoreModule.forRoot({myReducer: reducer}), 2 EffectsModule.forRoot([SearchEffects]), 3 StoreDevtoolsModule.instrument({ logOnly: environment.production}), 4 ], declarations: [AppComponent, EbayComponent, AmazonComponent, SearchComponent], providers: [ ProductService, {provide: LocationStrategy, useClass: HashLocationStrategy} ], bootstrap:[AppComponent] }) export class AppModule {}
In the next section, we’ll show you how to monitor state with the Chrome extension Redux DevTools and what the instrument() method is for.
The app component in the following listing remains the same as in the mediator example from chapter 8. Listing 8.10 contains annotations, so we won’t describe it here.
@Component({ selector: 'app-root', template: ` <div class="main"> <app-search></app-search> <p> <a [routerLink]="['/']">eBay</a> <a [routerLink]="['/amazon']">Amazon</a> <router-outlet></router-outlet> </div>`, styles: ['.main {background: yellow}'] }) export class AppComponent {}
To see this app in action, run npm install in the project mediator, and then run ng serve -o.
Your mediator app utilizes the packages @ngrx/store and @ngrx/effects, which can address most of your state-management needs. In the hands-on section, you’ll also use @ngrx/router-store, which offers bindings for connecting and monitoring Angular Router. There are other packages as well:
Consider exploring these packages on your own. The API of all ngrx packages is described at http://mng.bz/362y.
Now let’s see how to monitor app state with Redux DevTools.
Because you delegate state-management operations to ngrx, you need a tool to monitor state changes during runtime. The browser extension Redux DevTools along with the @ngrx/store-devtools package are used for the instrumentation of the app state. First, install @ngrx/store-devtools:
npm install @ngrx/store-devtools
Second, add the Chrome extension Redux DevTools (there is such an add-on for Firefox as well).
Third, add the instrumentation code to the app module. For example, for instrumentation with the default configuration, you can add the following line to the imports section of the @NgModule decorator:
StoreDevtoolsModule.instrument()
StoreDevtoolsModule must be added after StoreModule. If you want to add instrumentation minimizing its overhead in the production environment, you can use the environment variable as follows:
StoreDevtoolsModule.instrument({ logOnly: environment.production })
In production, set the logOnly flag to true, which doesn’t include tools like dispatching and reordering actions, persisting state and actions history between page reloads that introduces noticeable performance overhead. You can find the complete list of features that logOnly: true turns off at http://mng.bz/cOwC.
The instrument() method can accept the argument of type StoreDevtoolsConfig defined in the node_modules/@ngrx/store-devtools/src/config.d.ts file. The next code listing shows how to add instrumentation that will allow monitoring of up to 25 recent actions and work in log-only mode in the production environment.
StoreDevtoolsModule.instrument({ maxAge: 25, 1 logOnly: environment.production 2 })
You can also restrict some of the features of the Chrome Redux extension by providing the features argument to the instrument() method. For more details on configuring ngrx instrumentation and supported API, see http://mng.bz/3AXe, but here we’ll show you some Chrome Redux extension screenshots to illustrate some of the features of ngrx store DevTools.
If you run your app, but the Chrome Redux panel shows you a black window with the message “No store found,” refresh the page in the browser.
Launching the app creates the initial state in the store. Figure 15.7 shows the screen after you launch the mediator app and enter aaa in the input field. The sequence of actions starts with two init actions that are dispatched internally by the packages @ngrx/store and @ngrx/effects, and you select the @ngrx/store/init action on the left and the State button at the top right. The state properties searchQuery and searchResults are empty. To see the app state after one of the search actions is dispatched, click this action.
Think of init actions as hooks that your app can subscribe to and implement some logic upon app launch—for example, you can check whether the user is logged in. If your app uses lazy-loaded modules that have their own reducers, you may also see the @ngrx/store/update-reducer action for each newly loaded module, and its reducer will be added to the collection of store reducers.
Figure 15.8 shows the screen after clicking the Action button at the top right, and shows the type and payload of the latest action:
1. The latest action is "[Product] search success".
2. The Action tab is selected.
3. The action payload is stored in the state property searchResults.
4. The action type is "[Product] search success".
If your state object has many branches, by clicking (pin), you can pin a certain slice of the state to the top while browsing actions.
As shown in figure 15.9, after clicking the State button, you can see the current values of your state variables searchQuery and searchResults:
1. The latest action is "[Product] search success".
2. The State tab is selected.
3. The search criterion is stored in the state property searchQuery.
4. The search results are stored in the state property searchResults.
If the State tab shows the entire state object, clicking the Diff button shows what has changed as the result of the specific action. As shown in figure 15.10, if no action is selected, the Diff tab shows the state changes made by the latest action:
1. The latest action is "[Product] search success".
2. The Diff tab is selected.
3. The content of the state property searchResults is different.
While debugging an app, developers often need to re-create a certain state of the app, and one way to do that is to refresh the page and repeat user actions by clicking buttons, selecting list items, and so on. With Redux DevTools, you can travel back in time and re-create a certain state without refreshing the page—you can jump back to the state after a certain action occurred, or you can skip an action.
When you select an action, as shown in figure 15.11, you’ll see the Jump and Skip buttons, and then clicking Skip will strike through the selected action, and your running app will reflect this change. The Sweep button will be displayed at the top, and clicking it removes this action from the list. The Jump button jumps you to a specific state of the app for a selected action. Redux DevTools will show you the state properties at the moment, and the UI of the app will be rerendered accordingly:
1. The Skip button for this action has been clicked.
2. The State tab is selected.
3. The search query is aaabbb.
4. The state property searchResults shows no results for the aaabbb products.
5. The Sweep button was not clicked.
We’ve shown you the main features of the ngrx store DevTools, but to understand this tool better, we encourage you to spend some time playing with it on your own.
When a user navigates an app, the router renders components, updates the URL, and passes parameters or query strings if need be. Behind the scenes, the router object represents the current state of the router, and the @ngrx/router-store package allows you to keep track of the router state in the ngrx store.
This package doesn’t change the behavior of Angular Router, and you can continue using the Router API in components, but because the store should be a single source of truth, you may want to consider representing the router state there as well. At any given time, the ngrx store can give you access to such route properties as url, params, queryParams, and many others.
As with any other state properties, you’ll need to add a reducer to the ngrx store, and the good news is that you don’t need to implement it in your app because the routerReducer is defined in @ngrx/router-store. To add router state support, install this package first:
npm i @ngrx/router-store
After that, add StoreRouterConnectingModule to the NgModule decorator and add routerReducer to the list of reducers. StoreRouterConnectingModule holds the current router state. During navigation, before the route guards are invoked, the router store dispatches the action of type ROUTER_NAVIGATION that carries the RouterStateSnapshot object as its payload.
To get access to the routerReducer in your app, you need to perform two steps:
1. Give it a name by assigning a value to the property StoreRouterConnectingModule.stateKey.
2. Use the value from the previous step as the name of the routerReducer.
The following listing shows how the StoreRouterConnectingModule can be added to the app module. Here, you use myRouterReducer as the name of the routerReducer.
import {StoreRouterConnectingModule, routerReducer} from '@ngrx/router-store'; ... @NgModule({ imports: [ ... StoreModule.forRoot({myReducer: reducer, ? myRouterReducer: routerReducer}), 1 StoreRouterConnectingModule.forRoot({ stateKey: 'myRouterReducer' 2 }) ] ... }) export class AppModule { }
Now the state property myRouterReducer can be used to access the router state. The value of this property will be updated on each router navigation.
The app from section 15.2.3 didn’t include router state monitoring, but the source code that comes with this chapter has another app called mediator-router, which does monitor router state. Run this app and open the Redux DevTools panel. Then navigate to the Amazon route and you’ll see the ROUTER_NAVIGATION action and the myRouterReducer property in the app state object, as shown in figure 15.12.
By clicking the down arrow on the bottom toolbar, a QA engineer can save the current state of the app and send it to a developer, who can load it into the Redux extension (up arrow) to reproduce the scenario reported as a bug.
Expand the nodes of RouterStateSnapshot; it has lots and lots of properties. This object is so big that it may even crash Redux DevTools. Typically, you need to monitor just a small number of router state properties, and this is where the router state serializer comes in handy.
To implement the serializer, define a type that will include only those properties of RouterStateSnapshot that you want to monitor. Then write a class that implements the RouterStateSerializer interface, and @ngrx/router-store will start using it. This interface requires you to implement the serialize() callback, where you should destructure the provided RouterStateSnapshot to extract only those properties you care about.
Some of the properties, like url, are available at the top level, and others, such as queryParams, are sitting under the RouterStateSnapshot.root property. The mediator-router project implements a router state serializer in the serializer.ts file, as shown in the following listing.
import { RouterStateSerializer } from '@ngrx/router-store'; import {Params, RouterStateSnapshot} from '@angular/router'; interface MyRouterState { 1 url: string; queryParams: Params; } export class MyRouterSerializer implements RouterStateSerializer<MyRouterState> { 2 serialize(routerState: RouterStateSnapshot): MyRouterState { const {url, root: {queryParams}} = routerState; 3 return {url, queryParams}; 4 } }
Now Redux DevTools will show only the values of url and queryParams. To get the value of the router state object, use the select() operator. The next listing shows how you do it in the mediator-router project.
export class AppComponent { constructor(store: Store<any>) { store .select(state => state.myRouterReducer) 1 .subscribe(routerState => console.log('The router state: ', routerState)); } }
If you want to handle the router state action in your effects class, create an effect that handles the actions of type ROUTER_NAVIGATION. The following code listing from the effects.ts file from the mediator-router project shows how to do it in the effect.
@Injectable() export class SearchEffects { ... @Effect({ dispatch: false }) 1 logNavigation$ = this.actions$.pipe( ofType('ROUTER_NAVIGATION'), 2 tap((action: any) => { console.log('The router action in effect:', action.payload); }) ); constructor(private actions$: Actions, private productService: ProductService) {} }
In some cases, you may want to arrange navigation inside the effects class. For this, shown in the following listing, you can keep using the router API without any help from ngrx.
@Effect({ dispatch: false }) navigateToAmazon$ = this.actions$.pipe( ofType('GOTO_AMAZON') 1 tap((action: any) => { this.router.navigate('/amazon'); 2 }) );
This concludes our introduction to ngrx, but you’ll see how you use it in the ngAuction app in the hands-on section of this chapter.
Recently, one of our clients explained their needs for storing state. In their app, state is represented by a large object with nested arrays, and each array stores data rendered as a chart. The app retrieves one of the arrays, performs some calculations, and renders a chart. In the future, they plan to add new charts and arrays.
The client asked whether using a singleton Angular service with BehaviorSubject would offer a less scalable solution than ngrx for this use case. He added that in ngrx, they could use separate arrays (state slices) with their reducers, which could make adding new arrays and charts easier because ngrx automatically creates one global state object from individual reducers.
Let’s see if ngrx would help. First of all, if they needed lots of data to render the chart that doesn’t use the data directly, it could make sense to move computations to the server to avoid keeping huge objects in memory and calculating numbers in the browser. But what if they still wanted to implement all the data crunching on the client?
With the Angular service approach, the object with nested arrays would grow and become less maintainable. In the case of separate reducers/arrays, it would be easier to add them to the state and reason about the state.
But with the ngrx approach, the state object would also grow, and they’d need to add more reducers and selectors to handle the growth. With the Angular service approach, they could either add more methods for getting the state slices or could split the singleton into multiple services—the main one would store the data, and separate services (one per chart) would get and process the data from the main service.
Both ngrx and service approaches can do the job and remain maintainable. If the app doesn’t use ngrx yet, it wouldn’t make sense to use ngrx just because of charts.
Okay, is there a use case where ngrx offers advantages over the Angular service approach? Let compare three main features of state management:
If you want all these features, it can be easier to start with ngrx because ngrx enforces them. Without enforced discipline, your singleton service that started with 30 lines of code can quickly turn into an unmaintainable monster with hundreds of lines. If you’re not sure that best practices can be enforced in your team, go with ngrx, which offers a well-defined structure for your app.
Instead of writing the code for creating a store, effects, actions, and so on, you can generate them using Angular CLI, but first install the ngrx blueprints (a.k.a. schematics). You can read about using @ngrx/schematics at http://mng.bz/7W30. If your project was generated by Angular CLI 6 or newer, you can add NGRX artifacts to it by using the following commands:
ng add @ngrx/store ng add @ngrx/effects
These issues do exist, but Angular has all you need to address them. In your projects, all Angular components use the change detection strategy OnPush. When you need to modify a component’s data, create a new instance of an object bound to the component’s @Input.
There are cases when using the default change detection strategy makes more sense. For example, you may need to create a dynamic form, and its content changes depending on the values entered in other form controls. Control values are exposed as observables (as valueChanges), and if your component uses OnPush, and all other component properties are RxJS Subjects, it makes the code overly complex to express the logic in terms of RxJS operators.
The code definitely can be complex, but often it doesn’t have any benefits over disabling the OnPush strategy and mutating a component’s state directly. Then don’t use OnPush. On rare occasions, you can even manually trigger change detection with ChangeDetectorRef.
These techniques aren’t a replacement for the immutable ngrx state, and they don’t provide the same level of control over your data that ngrx does. But they help avoid problems caused by state mutation.
Actions and reducers introduce indirection and can quickly complicate your code if used without care. A new hire would need to spend more time to become productive with your app since each component can’t be understood in isolation. You may say the same is true for any Angular app that doesn’t use ngrx, but we would disagree for two reasons.
First, components and services look pretty much the same in every Angular app, but every ngrx project has its own approach for implementing and organizing actions, reducers, store selectors, and effects. Actions can be defined as variables, classes, interfaces, and enums. They can be directly exposed as ES module members or grouped into classes. The same applies to reducers.
Second, supporting actions and reducers requires writing additional code that wouldn’t exist in your app otherwise—it’s not just moving the existing app code from components to ngrx entities. If your components are already complex, using ngrx could make the code difficult to read.
ngrx considerably steepens the learning curve. You need to learn how the packages @ngrx/store and @ngrx/effects work. You may also want to learn the @ngrx/entity package that helps normalize relational data. If you have experience working with relational databases, you know how easy it is to join data located in related tables. Using @ngrx/entity eliminates the need to create nested JavaScript objects (for example, a customer object with nested orders) and write complex reducers.
You also need to be comfortable with the RxJS library. It’s not rocket science, but if you’re already in the process of learning Angular, TypeScript, and all the tooling, it would be wiser to postpone adding libraries that you can live without.
Good libraries are those that allow you to write less code. Currently, ngrx requires you to write lots of additional code, but we hope that future versions of ngrx will be simpler to implement and understand. Meanwhile, keep an eye on a promising state management library called NGXS (see https://ngxs.gitbooks.io/ngxs), which doesn’t require you to write as much code as ngrx and is built on TypeScript decorators. Another new project called ngrx-data (http://mng.bz/h6Nc) promises to support ngrx/Redux workflows with less coding.
Start with managing state using a singleton injectable service with BehaviorSubject. This approach may cover all your needs. Watch the video “When ngrx is an overkill” by Yakov Fain (www.youtube.com/watch?v=xLTIDs0CDCM), where he compares two small apps that manage state with and without ngrx. It’s never too late to add ngrx to your app, so don’t try to prematurely solve a problem you’re not facing yet.
Now let’s see how ngrx can be used in your ngAuction app.
The ngAuction app that comes with this chapter uses ngrx for state management. It’s modularized and has the root module AppModule and two feature modules, HomeModule and ProductModule. Since your feature modules are lazy loaded, we’ve added to each module the directory store, which in turn has its own subdirectories: actions, effects, and reducers, as shown in figure 15.13. Although this project has three directories named store, the running app will have a single store with merged states from each module.
Inside of each folder—actions, effects, and reducers—we have separate files related to specific slices of state. For example, you can find a separate search.ts file that implements a respective piece of the search functionality in each of those folders.
App state can represent not only data (such as the latest search query or results), but also the state of the UI (such as the loading indicator is shown or hidden). You may also be interested to know the current state of the router and the URL displayed by the browser.
Figure 15.14 depicts the combined state of the running ngAuction. The names of reducers are shown in bold italic font, and arrows point at the state properties handled by each reducer. In particular, the loading property of the products reducer could represent the state of the progress indicator. We’ll also add router support using the router reducer.
The router reducer is special in that you don’t need to implement it in your app because it’s defined in @ngrx/router-store, reviewed in the next section. Your ngAuction has the @ngrx/router-store package as a dependency in package.json.
Figure 15.15 shows a screenshot from Redux DevTools after ngAuction has launched and a user navigates to a specific product page. Note the router property there. The app state in figure 15.15 matches the state structure shown in figure 15.14:
To run the ngAuction that comes with this chapter, you’ll need to open two terminal windows, one for the client and one for server. Go to the server directory and run npm install there. Then, compile the code with the tsc command and start the server with the node build/main command. After that, open a separate Terminal window in the client directory and run the npm install command, followed by ng serve. We recommend you keep Redux DevTools open to monitor app state changes.
To keep the length of this section relatively short, we’ll review just the code that implements state management in the home module and give you a brief overview of the router state. The state management of the product module is implemented in a similar fashion.
ngAuction uses four ngrx modules: StoreModule, EffectsModule, StoreRouterConnectingModule, and StoreDevtoolsModule, and the package for each of these modules is included in the dependencies section of package.json. Let’s review the router-related code of the app module.
When you select a product, the router navigates to the corresponding product view, and the URL changes accordingly—for example, http://localhost:4200/products/1. Selecting another product will change the router state, and you can bind these types of changes to the app state as well. The next listing shows the code fragments from app.module.ts focusing on the code related to router state support.
import {EffectsModule} from '@ngrx/effects'; import {StoreRouterConnectingModule, routerReducer} from '@ngrx/router-store'; 1 import {StoreModule} from '@ngrx/store'; import {StoreDevtoolsModule} from '@ngrx/store-devtools'; import {environment} from '../environments/environment'; import {reducers, RouterEffects, 2 SearchEffects} from './store'; ... @NgModule({ imports: [ ... StoreModule.forRoot({...reducers, router: routerReducer}), 3 StoreRouterConnectingModule.forRoot({ 4 stateKey: 'router' 5 }), StoreDevtoolsModule.instrument({ name: 'ngAuction DevTools', logOnly: environment.production }), EffectsModule.forRoot([RouterEffects, SearchEffects]), 6 ... ], ... }) export class AppModule {
The next section provides more details about the line that loads reducers, while reviewing the code of the home module’s index.ts file.
The name of the router state within the store is defined by the property name (for example, router) mapped to the router reducer. In your app, you’ll use the default routerReducer and add it to the collection of app reducers:
StoreModule.forRoot({...reducers, router: routerReducer}),
The value of the stateKey property is used to find the router state within the store and connect it to Redux DevTools, so that time traveling during debugging works. The value assigned to stateKey (the router, in your case) must match the property name used in the map of reducers provided to the forRoot() method. To access a particular property of the router state, you can use the ngrx select operator on the object represented by the router variable.
Accessing the entire router state may crash Redux DevTools, which is why we created a custom router state serializer to keep in the store only the state properties you need. In the shared/services/router-state-serializer.service.ts file, we’ve implemented a serializer that returns an object containing only url, params, and queryParams. If we didn’t implement this serializer, the router state shown in figure 15.14 would have lots of nested properties.
When the home module is lazy loaded, its reducers are registered with the store, and its state object is merged with the root state. For this to happen, add the lines in the following listing to declare the store, reducer, and effects in the home.module.ts file
import {CategoriesEffects, ProductsEffects, reducers} from './store'; ... @NgModule({ imports: [ ... StoreModule.forFeature('homePage', reducers), 1 EffectsModule.forFeature([ CategoriesEffects, ProductsEffects ]) 2 ]
The difference between the methods forFeature() and forRoot() is that the latter also sets up the required providers for services from the StoreModule.
The home module has reducers in the files store/reducers/products.ts and store/reducers/categories.ts. Note that you import reducers not from a specific file, but from the directory store, and you can guess that this directory has a file named index.ts that combines and reexports reducers from several files. You’ll see the content of index.ts later in this section.
In ngAuction, CategoriesComponent serves as a container of the home view, which renders the category tabs and the product grid on the home view. Figure 15.16 shows that "[Products] Load All" is the first action dispatched by the app. Then it dispatches "[Categories] Load". When the data is loaded, two more actions are dispatched by the effects:"[Products] Load All Success" and "[Categories] Load Success".
Actions for categories are declared in the home/store/actions/categories.ts file, and actions for products in the home/store/actions/products.ts file. We’ll review only the content of home/store/actions/products.ts; the categories actions are declared in a similar way.
In ngAuction, each file with actions usually consists of three logical sections:
The next listing shows how actions are declared in the home/store/actions/products.ts file.
import {Action} from '@ngrx/store'; import {Product} from '../../../shared/services'; export enum ProductsActionTypes { 1 Load = '[Products] Load All', Search = '[Products] Search', LoadFailure = '[Products] Load All Failure', LoadSuccess = '[Products] Load All Success', LoadProductsByCategory = '[Products] Load Products By Category' } export class LoadProducts implements Action { 2 readonly type = ProductsActionTypes.Load; 3 } export class LoadProductsByCategory implements Action { 2 readonly type = ProductsActionTypes.LoadProductsByCategory; 3 constructor(public readonly payload: {category: string}) {} 4 } export class LoadProductsFailure implements Action { 2 readonly type = ProductsActionTypes.LoadFailure; 3 constructor(public readonly payload: {error: string}) {} 4 } export class LoadProductsSuccess implements Action { 2 readonly type = ProductsActionTypes.LoadSuccess; 3 constructor(public readonly payload: {products: Product[]}) {} 4 } export class SearchProducts implements Action { 2 readonly type = ProductsActionTypes.Search; 3 constructor(public readonly payload: {params: {[key: string]: any}}) {} 4 } export type ProductsActions 5 = LoadProducts | LoadProductsByCategory | LoadProductsFailure | LoadProductsSuccess | SearchProducts;
As you see, some of the action classes include only the action type, and some include the payload as well.
The code of the CategoriesComponent has changed compared to the chapter 14 version. Fragments of the categories.component.ts file related to state management are shown in listing 15.28. In the constructor of the CategoriesComponent, you subscribe to the route parameters. When this component receives the category value, it either dispatches the action to load the products of all categories or only the selected one.
import { getCategoriesData, getProductsData, 1 LoadCategories, LoadProducts, LoadProductsByCategory, 2 State } from '../store'; @Component({...}) export class CategoriesComponent implements OnDestroy { readonly categories$: Observable<string[]>; readonly products$: Observable<Product[]>; constructor(private route: ActivatedRoute, private store: Store<State>) { 3 this.products$ = this.store.pipe(select(getProductsData)); this.categories$ = this.store.pipe( 4 select(getCategoriesData), map(categories => ['all', ...categories]) 5 ); this.productsSubscription = this.route.params.subscribe( ({ category }) => this.getCategory(category) 6 ); this.store.dispatch(new LoadCategories()); 7 } private getCategory(category: string): void { return category.toLowerCase() === 'all' ? this.store.dispatch(new LoadProducts()) 8 : this.store.dispatch(new LoadProductsByCategory( 9 {category: category.toLowerCase()})); } }
The home module has two reducers: one for products and one for categories. The reducers and selectors for products are shown in the following listing.
import {Product} from '../../../shared/services'; import {ProductsActions, ProductsActionTypes} from '../actions'; export interface State { 1 data: Product[]; loading: boolean; loadingError?: string; } export const initialState: State = { 2 data: [], loading: false }; export function reducer(state = initialState, action: ProductsActions): State { switch (action.type) { case ProductsActionTypes.Load: { 3 return { ...state, loading: true, 4 loadingError: null }; } case ProductsActionTypes.LoadSuccess: { 5 return { ...state, data: action.payload.products, 6 loading: false, loadingError: null }; } case ProductsActionTypes.LoadFailure: { 7 return { ...state, data: [], 8 loading: false, loadingError: action.payload.error 9 }; } default: { return state; } } } export const getData = (state: State) => state.data; 10 export const getDataLoading = (state: State) => state.loading; 10 export const getDataLoadingError = (state: State) => state.loadingError;) 10
The state object for products has three properties: the array with products, the flag to control the loading indicator, and the text of the loading error, if any. When the reducer receives the action of type Load, it creates a new state object with an updated loading property that can be used by a component for showing a progress indicator.
If the LoadSuccess action has been dispatched, it indicates that the products were retrieved successfully. The reducer extracts them from the action’s payload property and updates the state’s data and loading properties. The LoadFailure action indicates that the products couldn’t be retrieved, and the reducer removes the data (if any) from the state object, updates the error message, and turns off the loading flag.
At the end of the reducer script for products, you see three lines with the functions that know how to access the data in the products state object. You define these functions here to keep them where the State interface is declared. These accessors are used to create selectors defined in index.ts.
The products reducer has no code that makes requests for data. Remember, the code communicating with external store parties is placed in the effects.
In general, the files named index.ts are used for reexporting multiple members declared in separate files. This way, if another script needs such a member, you import this member from a directory without the need to know the full path to a specific file. When reexporting members, you can give them new names and combine them into new types.
The home/store/reducers/index.ts file has the line import * as fromProducts from './products';, and to access exported members from the products.ts file, you can use the alias fromProducts as a reference—for example, fromProducts.State or fromProducts.getData(). With this in mind, let’s review the code of the home/store/reducers/index.ts file in the following listing.
import {createFeatureSelector, createSelector} from '@ngrx/store'; import * as fromRoot from '../../../store'; 1 import * as fromCategories from './categories'; 1 import * as fromProducts from './products'; 1 export interface HomeState { 2 categories: fromCategories.State; products: fromProducts.State; } export interface State extends fromRoot.State { 3 homePage: HomeState; 4 } export const reducers = { 5 categories: fromCategories.reducer, products: fromProducts.reducer }; // The selectors for the home module export const getHomeState = createFeatureSelector<HomeState>('homePage'); export const getProductsState = createSelector(getHomeState, state => state.products); export const getProductsData = createSelector(getProductsState, fromProducts.getData); export const getProductsDataLoading = createSelector(getProductsState, fromProducts.getDataLoading); export const getProductsDataLoadingError = createSelector(getProductsState, fromProducts.getDataLoadingError); export const getCategoriesState = createSelector(getHomeState, state => state.categories); export const getCategoriesData = createSelector(getCategoriesState, fromCategories.getData);
This script starts with creating descriptive alias names (for example, fromRoot), so it’s easier to read the code knowing where a particular member is coming from. Then you declare a HomeState interface combining all the properties declared in the State interfaces in the reducers for both products and categories.
The app store includes one state object that can be a complex object containing multiple branches. Each of these branches is created by a module reducer. When an action is triggered on the store, it goes through each registered reducer and finds the ones that have to handle this action. The reducer creates a new state and updates the corresponding branch of the global app state.
Here, you create a representation of the home module branch by declaring the State type that extends the State root and adding a new homePage property to it. You used this property in createFeatureSelector() and in listing 15.26 for registering the state object for the module home. When the combined app store is being formed, ngrx adds the homePage object to it.
The exported reducers member combines the reducers for products and categories. Now take another look at the app module in listing 15.25 which has the following line:
StoreModule.forRoot({...reducers, router: routerReducer})
Initially, the store finds and invokes each module reducer, which returns the corresponding state object. This is how the combined app state is created. The following code fragment from index.ts assigns the names categories and products to the respective slices of state:
export const reducers = { categories: fromCategories.reducer, products: fromProducts.reducer };
At the end of the script, you declare and export all the selectors that can be used for retrieving slices of the home module state. Note that you use the state accessor functions declared in the respective reducer files.
In the home module, effects are located in the files home/store/effects/categories.ts and home/store/effects/products.ts, and in the following listing, we review the code of the latter. The ProductsEffects class declares three effects: loadProducts$, loadByCategory$, and searchProducts$.
import {Injectable} from '@angular/core'; import {Actions, Effect, ofType} from '@ngrx/effects'; import {Action} from '@ngrx/store'; import {Observable, of} from 'rxjs'; import {catchError, map, switchMap} from 'rxjs/operators'; import {Product, ProductService} from '../../../shared/services'; import {LoadProductsByCategory, LoadProductsFailure, LoadProductsSuccess, ProductsActionTypes, SearchProducts} from '../actions'; @Injectable() export class ProductsEffects { constructor( private readonly actions$: Actions, private readonly productService: ProductService) {} @Effect() loadProducts$: Observable<Action> = this.actions$ .pipe( ofType(ProductsActionTypes.Load), 1 switchMap(() => this.productService.getAll()), 2 handleLoadedProducts() 3 ); @Effect() loadByCategory$: Observable<Action> = this.actions$ .pipe( ofType<LoadProductsByCategory>( 4 ProductsActionTypes.LoadProductsByCategory), 4 map(action => action.payload.category), 5 switchMap(category => this.productService.getByCategory(category)), 6 handleLoadedProducts() 7 ); @Effect() searchProducts: Observable<Action> = this.actions$ .pipe( ofType(ProductsActionTypes.Search), 8 map((action: SearchProducts) => action.payload.params), switchMap(params => this.productService.search(params)), handleLoadedProducts() ); } const handleLoadedProducts = () => 9 (source: Observable<Product[]>) => source.pipe( map(products => new LoadProductsSuccess({products})), catchError(error => of(new LoadProductsFailure({ error }))) );
Note the use of the <LoadProductsByCategory> type annotation in the ofType operator. This is one way of declaring the type of the action payload. Declaring the type explicitly (as in map((action: SearchProducts)) is another way to do this.
Figure 15.17 shows the state after the action of type LoadSuccess is dispatched:
1. The search state is empty.
2. The router state shows the URL and parameters.
3. The categories state will be populated after the load success for categories is dispatched.
4. The state of products has data retrieved from the server.
5. The loading flag is false.
6. There are no errors.
As usual, the actions dispatched by effects will be handled by the reducer, which will update the state with the data or error message.
Unit-testing state-related functionality is quite simple because only the reducer can change app state. Remember, a reducer is a pure function, which always returns the same output if the provided arguments are the same.
Because every action is represented by a class, you just need to instantiate the action object and invoke the corresponding reducer, providing the state and action objects to the reducer. After that, you assert that the state property under test has the expected value. For example, the home module has a reducer for products that defines the state object in the following listing.
export interface State { data: Product[]; 1 loading: boolean; 2 loadingError?: string; 3 }
Let’s review the code of the home/store/reducers/products.spec.ts file, shown in the following listing, which uses this state object and asserts that the loading flag is properly handled by the actions LoadProducts and LoadProductsSuccess.
import {LoadProducts, LoadProductsSuccess} from '../actions'; import {initialState, reducer} from './products'; describe('Home page', () => { describe('product reducer', () => { it('sets the flag for a progress indicator while loading products', () => { const loadAction = new LoadProducts(); 1 const loadSuccessAction = new LoadProductsSuccess({products: []}); 2 const beforeLoadingState = reducer(initialState, {} as any); 3 expect(beforeLoadingState.loading).toBe(false); 4 const whileLoadingState = reducer(beforeLoadingState, loadAction); 5 expect(whileLoadingState.loading).toBe(true); 6 const afterLoadingState = reducer(whileLoadingState, loadSuccessAction); 7 expect(afterLoadingState.loading).toBe(false); 8 }); }); });
When you invoke the reducer with the initial state, you provide an empty object and cast it to the type any, so regardless of the provided action, the reducer must return a valid state. Check the code of the reducer and note the default case in the switch statement there. Run the ng test command, and Karma will report that it executed successfully.
The listing 15.33 spec tests whether the reducer properly handles the loading property in the state object, without worrying about the action payload. But if you write a test for an action that has a payload, create a stub object with hardcoded data to simulate the payload and invoke the corresponding reducer.
This concludes our review of the ngrx code added to the home module of ngAuction. We encourage you to complete the code review of the product module on your own; its ngrx-related code is similar.
The authors started working on the first edition of this book when Angular was in its early alpha versions. Every new Alpha, Beta, and Release Candidate was full of breaking changes. Writing the second edition was easier because Angular became a mature and feature-complete framework. New major releases come twice a year, and switching from one release to another isn’t a difficult task. Every new release is tested against roughly 600 Angular apps used internally at Google to ensure backward compatibility.
We’d like to highlight some of the new features introduced in Angular 6 or planned for future releases:
<price-quoter stockSymbol="IBM"></price-quoter>As you can guess, the stockSymbol is an @Input parameter of the Angular price-quoter component. And if this component emits custom events via its @Output properties, your web page can listen to them using the standard browser API addEventListener(). In our opinion, this killer feature will open many enterprise doors to the Angular framework. Angular Elements will be officially released in Angular 7.
ng add @angular/material
ng generate @angular/material:materialNav --name=root-nav
We’re looking forward to all the new features that will make Angular even better!
With that, we’d like to thank you for reading our book. We hope you liked it and will leave positive feedback so that Manning will ask us to write a new edition in the future.
18.219.14.63