Chapter 15. Maintaining app state with ngrx

This chapter covers

  • A brief introduction to the Redux data flow
  • Maintaining your app state using the ngrx library
  • Exploring another implementation of the Mediator design pattern
  • Implementing state management in ngAuction with ngrx

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.

15.1. From a convenience store to Redux architecture

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.

15.1.1. What’s Redux?

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:

  • There’s a single source of truth. There’s a single store where your app contains the state that can be represented by an object tree.
  • State is read-only. When an action is emitted, the reducer function doesn’t update but clones the current state and updates the cloned object based on the action.
  • State changes are made with pure functions. You write the reducer function(s) that take an action and the current state object and return a new state.

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.

Figure 15.1. The 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.

Listing 15.1. An action to buy IBM stock
{
  type: 'BUY_STOCK',                         1
   stock: {symbol: 'IBM', quantity: 100}     2
 }

  • 1 The type of action
  • 2 The action payload

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.

Note

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.

Listing 15.2. A reducer signature
function (previousState, action): State {...}     1

  • 1 A reducer function returns a new state.

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.

15.1.2. Why storing app state in a single place is important

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:

  • URL parameters
  • HTML data* attributes
  • The browser’s local and session storage
  • Angular services
  • The React store

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!

Tip

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.

15.2. Introducing ngrx

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:

  • A single source of truth.
  • State is read-only.
  • State changes are made with pure functions.

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.

15.2.1. Getting familiar with 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.

Listing 15.3. The counter app without ngrx
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
   }
}

  • 1 Shows the counter value
  • 2 Increments the counter
  • 3 Decrements the counter

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.

Listing 15.4. reducer.ts
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
     }
}

  • 1 The initial value of the counter (state) is zero.
  • 2 Checks the action type
  • 3 Updates state by incrementing the counter
  • 4 Updates state by decrementing the counter
  • 5 Returns the existing state if an unknown action is provided

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.

Listing 15.5. app.module.ts
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 {}

  • 1 Lets the store know about the reducer for the app

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.

Listing 15.6. app.component.ts
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
   }
}

  • 1 Subscribes to the observable with the async pipe
  • 2 Declares the reference variable for the store observable
  • 3 select() emits changes in the counterState.
  • 4 Dispatches the INCREMENT action
  • 5 Dispatches the DECREMENT action

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.

Note

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.

Listing 15.7. child.component.ts
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);
    }
}

  • 1 Injects the store
  • 2 Subscribes to the store
  • 3 In 10 seconds, dispatches the INCREMENT action

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.

Figure 15.2. Running the counter app

To see this app in action, open the project counter, run npm install, and then run ng serve -o.

Note

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.

Eliminating the need for event bubbling

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?

Figure 15.3. EbayComponent compared to ChildComponent

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.

15.2.2. Getting familiar with effects and selectors

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.

Figure 15.4. ngrx data flow with 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”).

Note

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.

Listing 15.8. A fragment of a class with effects
@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.

Figure 15.5. Effects in the data flow

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.

15.2.3. Refactoring the mediator app with ngrx

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:

  • A store for storing and retrieving app state, search query, and results
  • A reducer for handing actions of type SEARCH and SEARCH_SUCCESS
  • Effects for handling actions of type SEARCH and SEARCH_SUCCESS
  • Selectors for retrieving the entire state object, search query, or search results

Figure 15.6 shows the mediator app after a user enters aaa in the search field of the SearchComponent.

Figure 15.6. The results of an aaa search on eBay

The store state

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.

Listing 15.9. The state of the mediator app
export interface State {
  searchQuery: string;           1
   searchResults: string[];      2
 }

  • 1 Payload of the SEARCH action dispatched by SearchComponent
  • 2 Payload of SEARCH_SUCCESS dispatched by the effect after invoking ProductService.getProducts()
Actions

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.

Listing 15.10. actions.ts
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

  • 1 Declares action types
  • 2 The class representing the search action with a payload
  • 3 The class representing the search-success action with a payload
  • 4 Declares the union SearchAction type

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 SearchComponent as action creator

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.

Listing 15.11. search.component.ts
@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
    });
  }
}

  • 1 Subscribes to the form control’s observable
  • 2 Instantiates and dispatches an action of type SEARCH with the payload

The dispatched action will be picked up by the reducer, which will update the searchQuery property on the state object.

Note

We’ll talk about another action creator, the SearchEffects class, later in this section.

The reducer

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.

Listing 15.12. reducers.ts
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
     }
  }
}

  • 1 Declares the structure of the state object
  • 2 Creates an object representing the initial state
  • 3 This action is dispatched by the component.
  • 4 Copies the existing state values into the new state object
  • 5 Updates two state properties with the new values
  • 6 This action will be dispatched by the effect.
  • 7 Copies the existing state values into the new state object
  • 8 Updates one state property with the new value
  • 9 Returns the current state if an unexpected action was dispatched
Tip

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.

Effects

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.

Listing 15.13. product.service.ts
@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
   }
}

  • 1 The counter concatenated to the search query is a product name.
  • 2 A function to generate a product name
  • 3 Creates a five-element array using productGenerator()
  • 4 Returns the observable of products after a one-second delay

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.

Listing 15.14. effects.ts
@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
 }

  • 1 Initializes the loadProducts$ effect with the stream/observable
  • 2 Executes a search only if the store dispatched the SEARCH action
  • 3 Extracts the payload from the action of type SEARCH
  • 4 Obtains the product based on the specified search query
  • 5 Dispatches the action of type SEARCH_SUCCESS with its payload
  • 6 Injects the ngrx Actions observable
  • 7 Injects the ProductService

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.

Tip

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))
    );
Selectors

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.

Listing 15.15. 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

  • 1 Creates a top-level selector of the top-level state
  • 2 Creates a selector for the state property searchQuery
  • 3 Creates a selector for the state property searchResults

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).

Listing 15.16. ebay.component.ts
@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
 }

  • 1 Subscribes to the observable that emits the search criteria and renders it
  • 2 Subscribes to the observable that emits products and renders them
  • 3 Invokes the getSearchQuery() selector on the store
  • 4 Invokes the getSearchResults() selector on the store
  • 5 Injects the store

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.

Listing 15.17. app.module.ts
@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 {}

  • 1 Configures the routes
  • 2 Registers the store and links it to the reducer
  • 3 Registers the effects
  • 4 Enables the use of Redux DevTools

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.

Listing 15.18. app.component.ts
@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.

What else ngrx has to offer

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:

  • @ngrx/entity is an entity state adapter for managing record collections.
  • @ngrx/schematics is a scaffolding library that provides blueprints for generating ngrx-related code.

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.

15.2.4. Monitoring state with ngrx store 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.

Listing 15.19. Adding instrumentation with two configuration options
StoreDevtoolsModule.instrument({
    maxAge: 25,                            1
     logOnly: environment.production       2
 })

  • 1 Retains the last 25 states in the browser extension
  • 2 Restricts the browser extension to logOnly mode in production

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.

Tip

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.

Figure 15.7. The store init action is selected.

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".

Figure 15.8. The Action tab view

Tip

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.

Figure 15.9. The State tab view

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.

Figure 15.10. The Diff tab is selected.

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.

Figure 15.11. The [Product] search-success action is skipped

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.

15.2.5. Monitoring the router state

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.

Listing 15.20. An app module fragment
import {StoreRouterConnectingModule, routerReducer}
  from '@ngrx/router-store';
...
@NgModule({
  imports: [
   ...
      StoreModule.forRoot({myReducer: reducer, ?
                           myRouterReducer: routerReducer}),      1
     StoreRouterConnectingModule.forRoot({
      stateKey: 'myRouterReducer'                                 2
     })
  ]
   ...
})
export class AppModule { }

  • 1 Adds routerReducer to the StoreModule
  • 2 Stores the name of the reducer in the stateKey property

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.

Tip

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.

Figure 15.12. The RouterStateSnapshot object in Redux DevTools

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.

Listing 15.21. serializer.ts
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
   }
}

  • 1 Defines the router state properties you want to monitor
  • 2 Creates a class that implements RouterStateSerializer
  • 3 Uses destructuring to get only the properties you need
  • 4 Returns an object with the properties url and queryParams

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.

Listing 15.22. app.component.ts
export class AppComponent {
  constructor(store: Store<any>) {
    store
      .select(state => state.myRouterReducer)              1
       .subscribe(routerState =>
           console.log('The router state: ', routerState));
  }
}

  • 1 Extracts a router state slice

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.

Listing 15.23. A fragment of effects.ts
@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) {}
}

  • 1 This effect doesn’t dispatch its own actions.
  • 2 Listens to the ROUTER_NAVIGATION action

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.

Listing 15.24. Navigating in effects
@Effect({ dispatch: false })
navigateToAmazon$ =
  this.actions$.pipe(
    ofType('GOTO_AMAZON')                    1
     tap((action: any) => {
      this.router.navigate('/amazon');       2
     })
  );

  • 1 Listens to the GOTO_AMAZON action
  • 2 Navigates to the /amazon route

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.

15.3. To ngrx or not to ngrx

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.

15.3.1. Comparing ngrx with Angular services

Okay, is there a use case where ngrx offers advantages over the Angular service approach? Let compare three main features of state management:

  • A single source of truth could mean two things:

    1. There’s only one copy of each set of data. This is easily achievable with an Angular service with BehaviorSubject.
    2. There’s a single object that keeps all the app data. This is a unique feature of the Redux/ngrx approach that enables Redux DevTools. This can be a valuable feature for large apps with cross-module interaction and lots of shared data. Without a single state object, it would be nearly impossible. DevTools allows exporting/importing the entire state of the app if you need to reproduce a bug found by a user or a QA engineer. But in the real world, state changes trigger side effects and don’t restore the app in exactly the same state.
  • State can be modified only by a reducer, so you can easily locate and debug an issue related to state. But if you use BehaviorSubject to keep data in your Angular services, you can do this as well. Without BehaviorSubject, it would be hard to identify all assignments that can modify state, but with BehaviorSubject, there’s a single place where you can put a breakpoint. Also, by applying the map operator to BehaviorSubject, you can handle all data modifications just like in a reducer.
  • With ngrx and specific selectors, you can produce a derived state that combines data from different parts of the store object, plus it can be memoized. You can easily do this in an Angular service as well. Define a service that injects other services, aggregates their values with combineLatest or withLatestFrom operators, and then emits the “derived” state.

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.

Tip

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

15.3.2. State mutation problems

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.

15.3.3. ngrx code is more difficult to read

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.

15.3.4. The learning curve

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.

15.3.5. Conclusion

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.

15.4. Hands-on: Using ngrx in ngAuction

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.

Figure 15.13. The state branches in the app and home modules

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.

Figure 15.14. The combined state object of ngAuction

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:

  • The State tab is selected.
  • You see a search slice of state, a router slice of state, a homePage slice of state, and a productPage slice of state.
Figure 15.15. The ngAuction state in Redux DevTools

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.

Note

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.

15.4.1. Adding the router state support to 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.

Listing 15.25. app.module.ts
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 {

  • 1 Imports the store module and the reducer for the router’s state
  • 2 Imports the router effects
  • 3 Adds the routerReducer to the collection of app reducers
  • 4 Adds the router state support
  • 5 Names the router state property as router
  • 6 RouterEffects listens to router events and dispatches ngrx actions handled by routerReducer.
Tip

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.

15.4.2. Managing state in the home module

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

Listing 15.26. A fragment from home.module.ts
import {CategoriesEffects, ProductsEffects, reducers} from './store';
...
@NgModule({
  imports: [
  ...
   StoreModule.forFeature('homePage', reducers),                         1
    EffectsModule.forFeature([ CategoriesEffects, ProductsEffects ])     2
   ]

  • 1 Registers the reducers for the feature home module
  • 2 Registers the effects for the feature home module
Tip

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.

Actions for products

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".

Figure 15.16. Loading products from all categories

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 enum containing string constants defining the action types. You can read about TypeScript enums at http://mng.bz/sTmp.
  • Classes for actions (one class per action) that implement the Action interface.
  • The union type that combines all action classes. You’ll use this type in reducers and effects, so the TypeScript compiler can check that the action types are correct, such as ProductsActionTypes.Load.

The next listing shows how actions are declared in the home/store/actions/products.ts file.

Listing 15.27. home/store/actions/products.ts
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;

  • 1 Declares allowed action types as the enum of string constants
  • 2 Declares the class for the action
  • 3 Declares the action type
  • 4 Uses the constructor argument to declare the action payload
  • 5 Declares the union type of allowed actions

As you see, some of the action classes include only the action type, and some include the payload as well.

CategoriesComponent

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.

Listing 15.28. Fragments from categories.component.ts
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()}));
  }
}

  • 1 Imports ngrx selectors
  • 2 Imports ngrx actions
  • 3 Injects the Store object
  • 4 Subscribes to the categories to be rendered as tabs
  • 5 Adds the all element to the array of category names
  • 6 Loads the selected or all categories
  • 7 Dispatches the action to load categories
  • 8 Dispatches the action to load all products
  • 9 Dispatches the action to load products by category
The reducer for products

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.

Listing 15.29. home/store/reducers/products.ts
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

  • 1 Declares the structure of the products state
  • 2 The initial state has no products, and the loading flag is false.
  • 3 Handles the Load action
  • 4 Updates the loading flag because the loading begins
  • 5 Handles the LoadSuccess action
  • 6 The products are loaded—updates the state with data.
  • 7 Handles the LoadFailure action
  • 8 Removes products data, if any
  • 9 Updates the error message
  • 10 The accessors returning state properties

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.

Note

The products reducer has no code that makes requests for data. Remember, the code communicating with external store parties is placed in the effects.

The role of index.ts in home reducers

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.

Listing 15.30. home/store/reducers/index.ts
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);

  • 1 Imports various exported members and gives them alias names
  • 2 Combines states from categories and products
  • 3 Declares the State type by extending it from the root State
  • 4 Declares a feature named homePage to be used with StoreModule or createFeatureSelector()
  • 5 Combines the reducers from categories and products

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.

Effects for products

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$.

Listing 15.31. home/store/effects/products.ts
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 })))
  );

  • 1 Processes only Load actions
  • 2 Tries to load all products
  • 3 Dispatches either LoadProductsSuccess or LoadProductsFailure
  • 4 Processes only LoadProductsByCategory actions
  • 5 Extracts the category from the payload
  • 6 Tries to load products by provided category
  • 7 Dispatches either LoadProductsSuccess or LoadProductsFailure
  • 8 Processes only Search actions
  • 9 The function to dispatch either LoadProductsSuccess or LoadProductsFailure

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.

Figure 15.17. The state after the LoadSuccess action

As usual, the actions dispatched by effects will be handled by the reducer, which will update the state with the data or error message.

15.4.3. Unit-testing ngrx reducers

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.

Listing 15.32. The products slice in the home module state
export interface State {
  data: Product[];            1
   loading: boolean;          2
   loadingError?: string;     3
 }

  • 1 Current products are stored here.
  • 2 If this flag is true, the UI should show a progress indicator.
  • 3 If loading fails, the error message is stored here.

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.

Listing 15.33. home/store/reducers/products.spec.ts
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
     });
  });
});

  • 1 Instantiates the LoadProducts action
  • 2 Instantiates the LoadProducts action
  • 3 Invokes the reducer with the initial state
  • 4 Asserts that the initial value of loading is false
  • 5 Invokes the reducer providing the current state and Load action
  • 6 Asserts that the loading flag is true
  • 7 Invokes the reducer providing the current state and LoadSuccess action
  • 8 Asserts that the loading flag is false

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.

Summary

  • The app state should be immutable.
  • The app logic can be removed from components and placed in effects and services.
  • A component’s methods should only send commands (actions) and subscribe to data for further rendering.
  • Although the ngrx learning curve is steep, using ngrx may result in better code organization, which is especially important in large apps.

Angular 6, 7, and beyond

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:

  • Angular ElementsAngular is a great choice for developing single-page applications, but creating a widget that can be added to an existing web page isn’t a simple task. The Angular Elements package will allow you to create a self-bootstrapping Angular component that’s hosted by a custom web element (see www.w3.org/TR/custom-elements/) that can be used in any HTML page. Simply put, you can define new DOM elements and register them with the browser. At the time of writing, all major browsers except Internet Explorer natively support custom elements; for IE, you should use polyfills. Say there’s an existing web app built using JavaScript and jQuery. The developers of this app will be able to use Angular components (packaged as custom elements) in the pages of such an app. For example, if you build a price-quoter component, Angular Elements will generate a script that can be added to an HTML page, and your component can be used on an HTML page. Here’s an example:
    <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.
  • Ivy rendererThis is the codename of a new renderer that will make the size of an app smaller and the compilation faster. The size of the Hello World app is only 7 KB minified and 3 KB gzipped. This renderer will eliminate unused code while building bundles, as opposed to optimizing bundles, as is currently done. The Ivy renderer will be introduced in Angular 8.
  • Bazel and Closure CompilerBazel is a fast build system used for nearly all software built at Google, including their 300+ apps written in Angular. Bazel makes it easier to publish Angular code that can be distributed as npm packages. The Closure Compiler is the bundling optimizer used to create JavaScript artifacts for nearly all Google web applications. The Closure Compiler consistently generates smaller bundles and does a better job of dead code elimination compared to Webpack and Rollup bundlers. By default, the Angular CLI project uses Webpack 4, which produces smaller bundles compared to its older versions.
  • Component Dev Kit (CDK)This package is already used by the Angular Material library, which offers 30+ UI components. Angular 6 introduces the tree component, which is good for displaying hierarchical data. The new flexible overlay component automatically resizes and positions itself based on viewport size. The badge component can show notification markers. What if you don’t want to use Angular Material but want to build your own library of UI components and control page layouts? You can do that with CDK. CDK contains multiple subpackages, including overlay, layout, scrolling, table, and tree. For example, the CDK table deals with rows and columns but doesn’t have its own styling. Although Angular Material adds styles to the CDK table, you can create your own according to your company guidelines. CDK supports responsive web design layout, eliminating the need for using libraries like Flex Layout, or learning CSS Grid. Angular 7 adds virtual scrolling for large lists of elements by rendering only the items that fit onscreen. Angular 7 also adds drag-and-drop support.
  • Angular CLIThe .angular-cli.json file is renamed to angular.json, and its structure changes. The ng update @angular/cli command automatically converts existing .angular-cli.json into angular.json. The ng update @angular/core command updates the dependencies in your project’s package.json to the latest version of Angular. If you need to upgrade your existing project to Angular 6, read Yakov Fain’s blog, “How I migrated a dozen projects to Angular 6 and then 7,” at http://mng.bz/qZwC. Upgrades from Angular 6 to 7 should not require code changes. The ng new library <name> command generates a project for creating a library instead of an app. This command will generate a library project with a build system and test infrastructure. The ng add command can add a package to your project, but in addition to what npm install does, it can also modify certain files in your project so you don’t need to do that manually. For example, the following command will install Angular Material, add a prebuilt theme to angular.json, add Material design icons to index.html, and add the BrowserAnimationsModule to the @NgModule() decorator:
    ng add @angular/material
  • Schematics and ng update—Angular CLI generates artifacts using a technology called Schematics, which uses code templates to generate various artifacts for your project. If you decide to create your own templates and have Angular CLI use them, Schematics will help you with this. The ng update command automatically updates your project dependencies and makes automated version fixes. With Schematics, you’ll be able to create your own code transformations, similar to ng update. But you’ll find some of the new prebuilt templates that come with Angular 6. For example, to generate all files for a root-nav component that already include the code of a sample Angular Material navigation bar, you can run the following command:
    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.

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

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