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.
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
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.
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
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};
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}
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
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
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}});
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})
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};
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) => { ... }
After
1export function reducer(state = initialState, action: operations.Actions): State {...}
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}
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
Create an app/reducers
directory and create a file for operations
:
1$ mkdir reducers
2$ cd reducers
3$ touch operations.ts
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};
From the code we have we can conclude that a module of the state has to export:
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
.
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};
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.
In your app/reducers
folder, create a new file named index.ts
.
1$ cd reducers
2$ touch index.ts
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}
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}
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 }
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 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}
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 }
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}
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);
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 }
_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}
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:
Let's tackle task #1 and add the currency
actions and reducer. With the architecture already laid, adding new functionality is simple.
Let's start with actions first. Create a new file in actions
named currencies.ts
.
1 $ cd actions
2 $ touch currencies.ts
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
Next, create a file currencies.ts
in app/reducers
. The currencies
.
1 $ cd reducers
2 $ mkdir currencies.ts
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}
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);
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 }
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
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}
Next, create a Currencies
component.
1touch currencies.component.ts
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}
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}
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
The standard for handling server-side requests requires the implementation of three actions:
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.LOAD_CURRENCIES_SUCCESS
and its payload will fill the rates
state property with the most recent information about currency rates.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
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.
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
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}
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.
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
Create an an app/effects
directory and a file for the effects concerning currencies
:
1 $ mkdir effects
2 $ cd effects
3 $ touch currencies.ts
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}
_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.
LoadRatesCompleteAction
will be created, having the rates as its payload.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}
Remember that we did not handle the case in which effects failed.
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
With the package installed, we can start implementing the pipe.
Create a file app/currency.pipe.ts
1$ touch currency.pipe.ts
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}
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}
CustomCurrencyPipe
to useFirst, 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}
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 -->
Go to http://localhost:4200 and have a look at the complete, multi-state Redux application we just created:
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.
In case you didn't understand what goes where, I have uploaded the final version of the example application for you to use as a reference.
Thanks for reading!