Author avatar

Hristo Georgiev

Building a Redux application with Angular 2 - Part 2

Hristo Georgiev

  • Sep 6, 2019
  • 36 Min read
  • 28,262 Views
  • Sep 6, 2019
  • 36 Min read
  • 28,262 Views
Front-End JavaScript

What we did last time

In part 1 of this guide, I covered Redux's main principles and terminology by creating a small application for recording financial operations.

This tutorial builds on the concepts learned in the previous one. If you need a refresher, I highly recommend looking over the first Redux application tutorial again.

Restructuring

In this part, the example application will be extended to include additional functionality.

To make this possible, the application must be able to have multiple states in its store. Implementing this feature requires serious alteration to many facets of the app.

In its current state, the application does not have a Meta-Reducer, which is essential for having more than one state. Meta-Reducers are a map of all the reducer functions in the state tree. It contains a reference for each of the state slices and their reducers. When an action gets dispatched, the Meta-Reducer goes through the map of all reducers, looking for the one that matches the action type and calls the function.

Another issue that causes concern is the actions and the reducer for operations are staying together, but as the applicaiton grows and the actions and the reducer are becoming more complex, there's going to be a lot of code in a single file. Thus, the actions and the reducers have to be divided in different files.

Having all these concerns in mind, here is how the directory structure will look in the new application:

1+-- app
2  |
3  +-- reducers
4  |   |
5  |   +--index.ts
6  +-- actions
7  |
8  +-- models

Models

Let's get started.

First, move operation.model.ts to the models folder. This is where the models for all entities of the application will be stored from now on.

"Classifying" actions

Next, the actions need to be overhauled, so we know to which reducer each action belongs. In operations.ts, delete the action constants and create a new file in actions/operations.ts'

1$ mkdir actions
2$ cd actions
3$ touch operations.ts
bash

Instead of making a new constant for each action, the actions will be grouped into enums.

1export const ActionTypes = {
2 ADD_OPERATION: 'Add an operation',
3 REMOVE_OPERATION:'Remove an operation',
4 INCREMENT_OPERATION:'Increment an operation',
5 DECREMENT_OPERATION: 'Decrement an operation',
6};
js

Action Class

Because the Meta Reducer dispatches actions to their respective reducers, we define custom actions and create actions as new class instances when dispatching using the @ngrx/store's Action class. Expressing actions as classes also enables type checking in reducer functions.

Here's how an ADD_OPERATION action would look like:

1export class AddOperationAction implements Action {
2  type = ActionTypes.ADD_OPERATION;
3
4  constructor(public payload:Operation) { }
5}
js

The action type is given by the ActionTypes enum that was previously defined and the payload of the action in the constructor fo the class.

Lastly, a type alias for all actions in order to be later used in the reducers.

1export type Actions
2  = AddOperationAction |
3  RemoveOperationAction |
4  IncrementOperationAction |
5  DecrementOperationAction
js

Here is the full code for operations actions:

1// app/actions/operations.ts
2
3import { Action } from '@ngrx/store';
4import {Operation} from "../models/operation.model";
5
6export const ActionTypes = {
7 ADD_OPERATION: 'Add an operation',
8 REMOVE_OPERATION:'Remove an operation',
9 INCREMENT_OPERATION:'Increment an operation',
10 DECREMENT_OPERATION: 'Decrement an operation',
11};
12
13export class AddOperationAction implements Action {
14 type = ActionTypes.ADD_OPERATION;
15
16 constructor(public payload:Operation) { }
17}
18
19export class RemoveOperationAction implements Action {
20 type = ActionTypes.REMOVE_OPERATION;
21
22 constructor(public payload:Operation) { }
23}
24
25export class IncrementOperationAction implements Action {
26 type = ActionTypes.INCREMENT_OPERATION;
27
28 constructor(public payload:Operation) { }
29}
30
31export class DecrementOperationAction implements Action {
32 type = ActionTypes.DECREMENT_OPERATION;
33
34 constructor(public payload:Operation) { }
35}
36
37
38export type Actions
39 = AddOperationAction |
40 RemoveOperationAction |
41 IncrementOperationAction |
42 DecrementOperationAction
ts

New form of dispatching

With new implementations of actions comes a new way of dispatching them.

Before

1//app.component.ts
2
3this._store.dispatch({type: ADD_OPERATION , payload: {
4  id: ++ this.id,//simulating ID increments
5  reason: operation.reason,
6  amount: operation.amount
7}});
ts

The task of directly sending an action with its type and payload will now be handled by the operation's class. Since the action is a class instance, dispatching is done by creating a new class member and putting the payload in the class constructor:

After

1//Import the action classes for operations
2import * as operations from "./common/actions/operations"
3
4
5this._store.dispatch(new operations.AddOperationAction({
6    id: ++ this.id,//simulating ID increments
7  reason: operation.reason,
8  amount: operation.amount
9})
ts

Adapting the reducer function

There are three problems with the current reducer:

First, the array state is too simple for our purposes. It needs to be replaced with an object which contains the array and adds it as an interface. This allows for the state to be more extendable in the future, when new features are implemented and when there is more data that has to be tracked.

1export interface State {
2  entities:Array<Operation>
3};
ts

Second, the operationsReducer is not a function, but a constant of type ActionReducer to which has a reducer function as a value. This was done because the constant was then directly passed to provideStore, without being referenced in a Meta Reducer.

Now that we've settled on implementing a Meta Reducer, we can rename operationsReducer to reducer and converted it to a function with a return type of State.

Before

1export const operationsReducer: ActionReducer = (state = initialState, action: operations.Actions) => { ... }
ts

After

1export function reducer(state = initialState, action: operations.Actions): State  {...}
ts

Third, the new action types are not implemented. To implement them, we'll import actions/operations and use ActionTypes from the imported library in the case statements.

Before

1switch (action.type) {
2  case ADD_OPERATION:
3    const operation:Operation = action.payload;
4    return [ ...state, operation ];
5}
ts

After

1switch (action.type) {
2  case operations.ActionTypes.ADD_OPERATION: {
3    const operation: Operation = action.payload;
4    return {
5      entities: [...state.entities, operation]
6    };
7  }
8  //... rest of the cases
ts

Create an app/reducers directory and create a file for operations:

1$ mkdir reducers
2$ cd reducers
3$ touch operations.ts
bash

The operations reducer after our above revisions:

1// app/reducers/operations.ts
2import '@ngrx/core/add/operator/select';
3import 'rxjs/add/operator/map';
4import 'rxjs/add/operator/let';
5import { Observable } from 'rxjs/Observable';
6import * as operations from '../actions/operations';
7import {Operation} from "../models/operation.model";
8
9
10/*
11 From a simple array ( [] ),
12 the state becomes a object where the array is contained
13 withing the entities property
14*/
15export interface State {
16 entities:Array<Operation>
17};
18
19const initialState: State = {  entities: []};
20
21/*
22Instead of using a constant of type ActionReducer, the
23function is directly exported
24*/
25export function reducer(state = initialState, action: operations.Actions): State {
26 switch (action.type) {
27   case operations.ActionTypes.ADD_OPERATION: {
28     const operation: Operation = action.payload;
29     /*
30     Because the state is now an object instead of an array,
31     the return statements of the reducer have to be adapted.
32     */
33     return {
34       entities: [...state.entities, operation]
35     };
36   }
37
38   case operations.ActionTypes.INCREMENT_OPERATION: {
39     const operation = ++action.payload.amount;
40     return Object.assign({}, state, {
41       entities: state.entities.map(item => item.id === action.payload.id ? Object.assign({}, item, operation) : item)
42     });
43   }
44
45   case operations.ActionTypes.DECREMENT_OPERATION: {
46     const operation = --action.payload.amount;
47     return Object.assign({}, state, {
48        entities: state.entities.map(item => item.id === action.payload.id ? Object.assign({}, item, operation) : item)
49     });
50   }
51
52   case operations.ActionTypes.REMOVE_OPERATION: {
53
54     return Object.assign({}, state, {
55       entities: state.entities.filter(operation => operation.id !== action.payload.id)
56     })
57   }
58   default:
59     return state;
60 }
61};
ts

From the code we have we can conclude that a module of the state has to export:

  1. A reducer function
  2. An interface that describes how the state looks like.

Creating a Meta Reducer

With the actions and the reducer adapted to the new standards, it is time to implement the Meta Reducer. If each of the reducer modules represent a table in a database, the Meta Reducer State interface represents the database schema, and the meta reducer function represents the database itself.

To have a clearer idea what happens behind the scenes in a Meta Reducer, we need to first look into the implementation of combineReducers.

combineReducers

1const combineReducers = reducers => (state = {}, action) => {
2  return Object.keys(reducers).reduce((nextState, key) => {
3    nextState[key] = reducers[key](state[key], action);
4    return nextState;
5  }, {});
6};
ts

combineReducers is a function that takes an object with all the reducer functions as property values and extracts its keys. Then it uses Array.reduce() to accumulate the return value of each of the reducer functions into a state tree and reassign it to the key to which the reducer corresponds. The return value is an object which contains the key-value pairs of the reducers and the states they returned.

Implementation

In your app/reducers folder, create a new file named index.ts.

1$ cd reducers
2$ touch index.ts
bash

In index.ts, paste the following snippet:

1import {combineReducers, ActionReducer} from '@ngrx/store';
2import {Observable} from "rxjs";
3import {compose} from "@ngrx/core";
4
5/*
6 Import each module of your state. This way, you can access
7 its reducer function and state interface as a property.
8*/
9import * as fromOperations from '../reducers/operations';
10
11/*
12 The top-level interface of the state is simply a map of all the inner states.
13 */
14export interface State {
15  operations: fromOperations.State;
16}
17
18/* The reducers variable represents the map of all the reducer function that is used in the Meta Reducer */
19const reducers = {
20  operations: fromOperations.reducer,
21};
22
23/* Using combineReducers to create the Meta Reducer and export it from the module. The exported Meta Reducer will be used as an argument in provideStore() in the application's root module.
24*/
25const combinedReducer: ActionReducer<State> = combineReducers(reducers);
26
27export function reducer(state: any, action: any) {
28    return combinedReducer(state, action);
29}
ts

The final step of the implementation is to put the reducer as an argument in provideStore()

1// app/app.module.ts
2import {reducer} from "./common/reducers/index";
3
4@NgModule({
5  bootstrap: [ AppComponent ],
6  declarations: [
7   //...
8  ],
9  imports: [
10    /*
11     Put the reducer as an argument in provideStore
12    */
13    StoreModule.provideStore(reducer),
14  ],
15})
16export class AppModule {
17  constructor() {}
18}
ts

Getting state slices

The new architecture of the application requires a different way of accessing the state slices.

Previously, we could access operations using the following snippet:

1//app.component.ts
2 import {State, Store} from "@ngrx/store";
3
4 export class AppComponent {
5
6 //....
7
8 constructor(private _store: Store<State>) {
9   this.operations = _store.select('operations')
10 }
ts

However, now there's a state tree, and the operations state is an object that could have multiple properties. Additionally, our current way doesn't allow combining states in a specified manner. There needs to be a set of designated functions that return certain parts of the state tree.

Accessing parts of the state tree.

Accessing the state slices is not going to be done directly from the application's components. Instead, select-ing of the state will be done in the app/reducers modules.

To illustrate how this works, let's first select operations from the state tree:

1 //app/reducers/index.ts
2
3 export function getOperations(state$: Observable<State>) {
4   return state$.select(state => state.operations);
5}
ts

Accessing the state properties

Using the last function, only the operations state of the application is accessed, and it contains this:

1 {
2    entities: [ //...an array of operations
3 }
js

That's a problem, because we need a particular property of the operations state - the entities array. Here is how we can grab that:

First, in operations.ts, export a function which accesses the entities property of the operations state:

1// app/reducers/operations.ts
2
3/*
4 Get the entities of the operations state object. This function will be
5 imported into the file for the Meta Reducer, where it will
6 be composed together with a function that gets the state of
7 the  operations state object out of the application state.
8*/
9
10export function getEntities(state$: Observable<State>) {
11return state$.select(s => s.entities);
12}
ts

Second, compose getOperations and getEntities. Function composition is one of the building blocks of functional programming. It executes a set of functions, putting the returned value of the first function as the argument for the second function. In math, composition of two functions f(x) and g(x) would result in f(g(x)).

compose() applies functions from right to left.

1// app/reducers/index.ts
2
3export const getEntities = compose(fromOperations.getEntities, getOperations);
ts

In this case, getOperations first accesses the operations state from the state tree and then getEntities gets the entities property of getOperations's returned operation state.

Let's apply this functionality in app.component.ts:

1//app.component.ts
2
3/*
4 In order to access the application state, reference the reducers folder again,
5 accessing all the exported members from it though index.ts
6 */
7import * as fromRoot from './common/reducers';
8
9export class AppComponent {
10
11  //...
12
13  constructor(private _store: Store<fromRoot.State>) {
14    this.operations = _store.let(fromRoot.getEntities)
15  }
js

_store.let executes getEntities and returns its value.

Here is how the new AppComponent looks like with the new state selection and action dispatching implemented:

1import { Component } from '@angular/core';
2import {State, Store} from "@ngrx/store";
3import {Operation} from "./common/models/operation.model";
4import * as operations from "./common/actions/operations"
5import * as fromRoot from './common/reducers';
6
7@Component({
8  selector: 'app-root',
9  template: `
10      <div class="container">
11            <new-operation (addOperation)="addOperation($event)"></new-operation>
12            <operations-list [operations]="operations| async"  
13            (deleteOperation)="deleteOperation($event)"
14            (incrementOperation)="incrementOperation($event)"
15            (decrementOperation)="decrementOperation($event)"></operations-list>
16      </div>
17`
18})
19export class AppComponent {
20
21  public id:number = 0 ; //simulating IDs
22  public operations:Observable<Operation[]>;
23
24  constructor(private _store: Store<fromRoot.State>) {
25    this.operations = this._store.let(fromRoot.getEntities)
26  }
27
28  addOperation(operation) {
29    this._store.dispatch(new operations.AddOperationAction({
30        id: ++ this.id,//simulating ID increments
31      reason: operation.reason,
32      amount: operation.amount
33    })
34    );
35  }
36
37  incrementOperation(operation){
38    this._store.dispatch(new operations.IncrementOperationAction(operation))
39  }
40
41  decrementOperation(operation) {
42    this._store.dispatch(new operations.DecrementOperationAction(operation))
43  }
44
45  deleteOperation(operation) {
46    this._store.dispatch(new operations.RemoveOperationAction(operation))
47  }
48}
js

Adding a new state

It's time to take the example application to the next level by adding multi-currency support. Being able to see the operations in different currencies comes with certain requirements:

  1. Reducer - stores the available currencies and the currently selected currency.
  2. JSON API - provides up-to-date currency rates.
  3. Technique - reactively changes amounts in the operations.

Let's tackle task #1 and add the currency actions and reducer. With the architecture already laid, adding new functionality is simple.

Actions

Let's start with actions first. Create a new file in actions named currencies.ts.

1 $ cd actions
2 $ touch currencies.ts
bash

The first action will be when the user attempts to change the currency. Here's how the code for the action looks:

1// app/actions/currencies.ts
2
3import { Action } from '@ngrx/store';
4
5export const ActionTypes = {
6  CHANGE_CURRENCY: 'Change currency',
7};
8
9export class ChangeCurrencyAction implements Action {
10  type = ActionTypes.CHANGE_CURRENCY;
11  constructor(public payload:string) { }
12}
13
14export type Actions =
15  ChangeCurrencyAction
ts

Reducer

Next, create a file currencies.ts in app/reducers. The currencies .

1 $ cd reducers
2 $ mkdir currencies.ts
bash

Here is what the currencies reducer looks like:

1// app/reducers/currencies.ts
2
3import '@ngrx/core/add/operator/select';
4import { Observable } from 'rxjs/Observable';
5import * as currencies from '../actions/currencies';
6
7/*
8The sate object needs to have three properties:
91. A property for keeping a list of the available currencies
102. A property for keeping the selected currency
113. A property for keeping the list of exchange rates
12*/
13export interface State {
14  entities:Array<string>
15  selectedCurrency: string | null;
16  rates: Array<Object>,
17};
18
19const initialState: State = {
20    entities: ['GBP', 'EUR'],
21    selectedCurrency: null,
22    rates: [] ,
23};
24
25export function reducer(state = initialState, action: currencies.Actions): State {
26  switch (action.type) {
27
28    case currencies.ActionTypes.CHANGE_CURRENCY: {
29        return {
30          entities: state.entities,
31          selectedCurrency: action.payload,
32          rates: state.rates
33        };
34    }
35
36    default:
37      return state;
38  }
39}
40
41/*
42 These selector functions provide access to certain slices of the currency state object.
43*/
44
45export function getCurrenciesEntities(state$: Observable<State>) {
46  return state$.select(s => s.entities);
47}
48
49export function getSelectedCurrency(state$: Observable<State>) {
50  return state$.select(s => s.selectedCurrency);
51}
52
53export function getRates(state$: Observable<State>) {
54  return state$.select(s => s.rates);
55}
ts

The final step is to include the currencies state in the store:

1// app/reducers/index.ts
2
3// Import app/reducers/currencies.ts
4
5import * as fromCurrencies from '../reducers/currencies';
6//...
7
8export interface State {
9  operations: fromOperations.State;
10  //Add the currencies state interface
11  currencies: fromCurrencies.State;
12}
13
14const reducers = {
15  operations: fromOperations.reducer,
16
17  // Add the currency reducer
18  currencies: fromCurrencies.reducer
19};
20
21//...
22
23//Access the 'currencies' state in the application store
24export function getCurrencies(state$: Observable<State>) {
25  return state$.select(state => state.currencies);
26}
27
28// Access 'entities' from the 'currencies' state in the application state.
29export const getCurrencyEntities = compose(fromCurrencies.getCurrenciesEntities , getCurrencies);
30
31// Access 'selectedCurrency' from the 'currencies' state in the application state.
32export const getSelectedCurrency = compose(fromCurrencies.getSelectedCurrency , getCurrencies);
33
34// Access 'rates' from the 'currencies' state in the application state.
35export const getCurrencyRates = compose(fromCurrencies.getRates , getCurrencies);
ts

Accessing the new state

Before creating a 'dumb' component to display the currency options and the selected currency, the currencies have to be retrieved in the smart component (in this case AppComponent).

1// app/app.component.ts
2
3export class AppComponent {
4  //...
5  public currencies:Observable<string[]>;
6  public selectedCurrency: Observable<string>;
7
8  constructor(private _store: Store<fromRoot.State>) {
9    //...
10    this.currencies = this._store.let(fromRoot.getCurrencyEntities);
11    this.selectedCurrency =this._store.let(fromRoot.getSelectedCurrency);
12  }
13  //...
14    onCurrencySelected(currency:string) {
15    this._store.dispatch(new currencies.ChangeCurrencyAction(currency))
16  }
17 }
ts

Apart from making component class variables for accessing the currencies in the store, there is also a function for dispatching the ChangeCurencyAction() when the user selects another currency.

Next, install ng-bootstrap in order to add good-looking, interactive radio buttons.

1 $ npm install --save @ng-bootstrap/ng-bootstrap
bash

And import the NgbModule in the AppModule:

1// app.module.ts
2import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
3
4//...
5@NgModule({
6  declarations: [AppComponent, ...],
7  imports: [NgbModule.forRoot(), ...],
8  bootstrap: [AppComponent]
9})
10export class AppModule {
11}
ts

Currencies component

Next, create a Currenciescomponent.

1touch currencies.component.ts
bash
1// currencies.component.ts
2
3import {Component, Input, ChangeDetectionStrategy, Output, EventEmitter} from '@angular/core';
4
5@Component({
6    selector: 'currencies',
7    template: `
8    '<div [ngModel]="selectedCurrency" (ngModelChange)="currencySelected.emit($event)" ngbRadioGroup name="radioBasic">
9    <label *ngFor="let currency of currencies" class="btn btn-primary">
10    <input type="radio" [value]="currency"> {{currency}}
11    </label>
12    </div>`,
13     changeDetection: ChangeDetectionStrategy.OnPush
14})
15export class Currencies  {
16    @Input() currencies:Array<string>;
17    @Input() selectedCurrency:string;
18    @Output() currencySelected = new EventEmitter();
19
20    constructor() { }
21}
ts

The Currencies component is a 'dumb' component (no logic) that uses ngbRadioGroup to display the available currencies. (ngModelChange) is used to emit an event to AppComponent every time the user clicks on the radio buttons.

Lastly, add Currencies to AppModule:

1// app.module.ts
2import {Currencies} from './currencies.component';
3
4//...
5@NgModule({
6  declarations: [Currencies, ...],
7  //...
8})
9export class AppModule {
10}
ts

Effects

If you go to http://localhost:4200/ now and play around with the currency buttons, you'll see that nothing special happens. Looking at the current state of the application, there needs to be a way to load the currency rates from a dedicated currency data API, such as fixer.io.

Redux has its own convention for handling server-side requests. It does it through a middleware - a piece of logic that stays between the server and the reducer functions. In Redux, server-side requests are regarded as side effects from actions that cannot be entirely handled through a reducer. Such actions call impure functions, making the reducers unable to handle the recreation of the action because the function does not depend entirely on its input. Read more about this important concept here

Actions for handling side effects

The standard for handling server-side requests requires the implementation of three actions:

  1. Action that indicates the start of the server-side request: This action is dispatched just before the request is made. In the example application, the name of this action will be named LOAD_CURRENCIES. A reducer handling such an action would change a dedicated state property for indicating a server-side location such as loadingCurrencies, which can be used to implement a loading spinner, for example.
  2. Action that indicates a successful request: This action is dispatched when te request is done. Its payload contains the request response. The reducer simply adds the response to its corresponding state property. In the example, this action name will be called LOAD_CURRENCIES_SUCCESS and its payload will fill the rates state property with the most recent information about currency rates.
  3. Action that indicates a failed request This action is dispatched if the request fails. The action payload may contain the error reason or simply return nothing. In the example application, this action would be normally called LOAD_CURRENCIES_FAIL

For the sake of simplity, we'll omit the process of making a separate action that handles failure. Here is what the implementation of the new action looks like:

1import { Action } from '@ngrx/store';
2
3export const ActionTypes = {
4  CHANGE_CURRENCY: 'Change currency',
5  LOAD_CURRENCY_RATES: 'Loading currency rates'
6};
7
8export class LoadCurrencyRatesAction implements Action {
9  type = ActionTypes.LOAD_CURRENCY_RATES;
10  constructor(public payload:string) { }
11}
12
13export class LoadRatesCompleteAction implements Action {
14  type = ActionTypes.LOAD_RATES_COMPLETE;
15  constructor(public payload:string) { }
16}
17
18export type Actions =
19  // Add the functions as tpyes
20  LoadCurrencyRatesAction |
21  LoadRatesCompleteAction
ts

What happens between the Load action and the Load Success/Failure action?

This is where the middleware comes into play. To be more exact, the middleware for handling side effects.

As for the server-side calls themselves, they will be handled by an Angular 2 service, which will be called within the effect.

Fetching data with a service

Before adding the effects, let's add the service that is going to fetch the data from the fixer.io API.

Create an app/services directory that will contain the services which handle server-side requests and create a service for currencies:

1$ mkdir services
2$ cd services
3$ touch currencies.ts
bash

The service looks like this:

1import {Http} from '@angular/http';
2import {Injectable} from '@angular/core';
3
4@Injectable()
5export class CurrencyService {
6
7  constructor(private http: Http ) {}
8
9  loadCurrencies() {
10    //Inferring that the base is USD
11    return  this.http.get('http://api.fixer.io/latest?base=USD' )
12      .map((response) => {
13         let body = response.json();
14          return body.rates
15
16      })
17  }
18}
ts

CurrencyService contains one function, loadCurrencies(), which makes a simple HTTP request to the fixer.io API and returns the rates property of the response as an observable.

Implementing an effect

Next, we are going to implement the side-effect for handling the returned rates from CurrencyService.

In Angular 2, there is a special package for handling side effects - ngrx/effects

Open your terminal and type:

1npm install @ngrx/effects --save
bash

Create an an app/effects directory and a file for the effects concerning currencies:

1 $ mkdir effects
2 $ cd effects
3 $ touch currencies.ts
bash
1// app/effects/operations.ts
2
3import { Injectable } from '@angular/core';
4import { Effect, Actions } from '@ngrx/effects';
5import { Observable } from 'rxjs/Observable';
6import * as currencyActions from '../actions/currencies';
7
8import {CurrencyService} from "../services/currency.service";
9import {LoadRatesCompleteAction} from "../actions/currencies";
10
11@Injectable()
12export class CurrencyEffects {
13  constructor(
14    private _actions: Actions,
15    private _currencyService:CurrencyService
16  ) { }
17
18/*
19 The effects for different states are singletons that 'intercept' dispatched actions that are being sent to the reducer.
20*/
21
22  @Effect() loadCategories$ = this._actions.ofType(currencyActions.ActionTypes.LOAD_CURRENCY_RATES)
23    .switchMap(() => this._currencyService.loadCurrencies()
24      .map((rates) => new LoadRatesCompleteAction(rates) )
25      .catch(() => Observable.of( new LoadRatesCompleteAction({})))
26    );
27}
ts

_actions.ofType() returns an observable which watches for newly dispatched events that match the type of the action. switchMap() is an rxJS operator. It is part of several operators that are used for reactive programming. What makes switchMap() different is that it returns only the most recent observable's value in a stream of observables.

Once the service returns, there are a few options.

  • If the results are successfully fetched, a new LoadRatesCompleteAction will be created, having the rates as its payload.
  • If an error has occured, the same action will be created, but with empty payload.
  • Alternatively, a LoadRatesFailAction can be added to currencies/action for handling cases in which the server fails to return a result.

The final step for handling effects to register the effects to the app.module and the currency as a provider:

1// app.module.ts
2import {CurrencyEffects} from "./common/effects/currencies";
3import {EffectsModule} from "@ngrx/effects";
4import { CurrencyService} from "./common/services/currency.service";
5//...
6
7@NgModule({
8  //...bootstrap and declarations
9
10  imports: [
11    // Add each the effects for each of your states in the module.
12    EffectsModule.run(CurrencyEffects),
13  ],
14  //Add the CurrencyService as a provider
15  providers: [CurrencyService]
16})
17export class AppModule {
18  constructor() {}
19}
ts

Remember that we did not handle the case in which effects failed.

Taking advantage of the reactive state

One of the best features of Redux is that it gives the opportunity for the most recent part of the application state to be accessed anywhere in the application.

To illustrate this, let's finish the current implementation of the currencies in the applications. So far, the currency rates are registered the part of the state and thanks to effects, they can be asynchronously added to the currencies state.

In order to makeuse of the rates, a custom pipe will be implemented to handle the currency conversion depending on the selected currency. There's no need to perform any special mathematical operations - money.js is going to be used to handle the conversion of the currencies.

First, install 'money.js':

1npm install money --save
bash

With the package installed, we can start implementing the pipe. Create a file app/currency.pipe.ts

1$ touch currency.pipe.ts
bash

The pipe needs to get the latest state of the rates property in the currencies state add convert it for each of the values the pipe is applied to.

1import { Pipe, PipeTransform } from '@angular/core';
2import * as fromRoot from './common/reducers';
3import {State, Store} from "@ngrx/store";
4
5/*
6 Requiring money.js and setting the
7 base currenc to USD. In this case, we infer that
8 the base currency is USD. However, if you add a
9 baseCurrency attribute to the currencies state, you
10 can make the base currency dynamic as well.
11*/
12const fx = require('money');
13fx.base = "USD";
14
15@Pipe({
16  name: 'currencyPipe',
17})
18export class CustomCurrencyPipe implements PipeTransform {
19  /*
20   One of the main advantages of Redux is that the state of
21   the application can be observed from any file by simply
22   implementing a selector and calling it where needed.
23  */
24  constructor(private _store: Store<fromRoot.State>) {
25    this._store.let(fromRoot.getCurrencyRates).subscribe((rates) => {
26      fx.rates = rates;
27    });
28  }
29  /*
30   The currency parameter obtains its value from the selectedCurrency
31   property. An alternative implementation would be to call
32   getSelectedCurrency within the pipe and get the selectedCurrency
33   within the pipe.
34  */
35  transform(value: number , currency): string {
36      if(currency != null) {
37        value = fx.convert(value,  {from: "USD" , to: currency});
38        return currency + ' ' + value;
39      } else {
40        return 'USD' + ' ' + value ;
41      }
42  }
43}
ts

Declare the pipe in app.module.ts:

1import {CustomCurrencyPipe} from "./currencyPipe";
2//...
3
4@NgModule({
5  declarations: [s
6    CustomCurrencyPipe
7  ],
8  //...
9})
10export class AppModule {
11  constructor() {}
12}
ts

Putting CustomCurrencyPipe to use

First, add selectedCurrency in two places in the AppComponent template - as an input for the Currencies, which will need it to display the active currency, and the OperationsList components, which will use it in CustomCurrencyPipe.

1// app.component.ts
2@Component({
3  selector: 'app-root',
4  template: `
5      <div class="container">
6            <new-operation (addOperation)="addOperation($event)"></new-operation>
7            <!-- Add selectedCurrency as an input for the currencies component -->
8            <currencies (currencySelected)="onCurrencySelected($event)" [currencies]="currencies | async" [selectedCurrency]="selectedCurrency | async"></currencies>
9            <!-- Add selectedCurrency as an input for the operations-list component -->
10            <operations-list [operations]="operations| async"
11            [selectedCurrency]="selectedCurrency | async"
12            (deleteOperation)="deleteOperation($event)"
13            (incrementOperation)="incrementOperation($event)"
14            (decrementOperation)="decrementOperation($event)"></operations-list>
15      </div>
16`
17})
18export class AppComponent {
19//..
20}
ts

Finally, in OperationsList apply the CustomCurrencyPipe with the selectedCurrency as a parameter:

1<!-- operations-list.template.html -->
2 <!-- ...wrapper tags -->
3    <ul class="list-group" >
4      <li *ngFor="let operation of operations"class="list-group-item" [ngClass]="{'list-group-item-success': operation.amount > 0 ,'list-group-item-danger': operation.amount < 0 }">
5      <!-- Apply the currencyPipe to each of the operation amounts in the operations list -->
6        <h3 class="h3">{{operation.amount | currencyPipe: selectedCurrency}}</h3>
7       <!-- ...buttons -->
8      </li>
9    </ul>
10 <!-- ...closing wrapper tags -->
html

The final result

Go to http://localhost:4200 and have a look at the complete, multi-state Redux application we just created:

description

Conclusion

Redux is a new approach for managing application state. The concepts from functional programming applied in Redux may be suitable for certain use cases, and it's up to you your to decide whether Redux is the right way to go for your next project.

Got lost in the code?

Thanks for reading!