What is a Redux pattern in Angular

Manfred Steyer

The @ ngrx / store library enables the Redux pattern to be used in Angular 2. Through the consistent use of immutables and observables, performance improvements can also be achieved.

After the first part of this article has motivated the Redux approach and introduced the @ ngrx / store library, this continuation illustrates its use based on a case study for managing flight bookings (see figure). There is one of three statuses per booking: booked, checked in or boarded. The example presents the status per booking as well as a summary at the beginning of the page. It shows how many postings there are for each state. All of the source code is available on GitHub.

Single Immutable State Tree

The Redux pattern provides a central data structure for managing the overall health of the application. The case study describes this so-called single immutable state tree via the interface AppState:

export interface AppState {
boarding: BoardingState;

// ... further properties could be used here ...
// ... for other parts of the application ...
// ... be defined ...
}

The interface provides the property for the application under consideration boarding that represents a branch of the tree. Developers could introduce additional properties for further use cases. The interface BoardingState describes the structure of the branch under consideration. It defines that in addition to the bookings called up, statistics must also be managed:

export interface BoardingState {
bookings: Array ,
statistics: BoardingStatistic;
}

export interface BoardingStatistic {
countBoarded: number;
countCheckedIn: number;
countBooked: number;
}

Administration via reducer

The demo application contains a reducer to manage the status. It is located in the TypeScript module boarding.reducerbased, among other things, on types from the @ ngrx / store library. As usual in the Angular 2 environment, it can be found on npm:

npm install @ ngrx / store --save

The module first defines constants to identify individual actions. It then generates the initial status for the managed application area in the form of an object that is located on the interface BoardingState oriented:

// boarding.reducer.ts (part 1)

import {Reducer, Action} from '@ ngrx / store';
import {BoardingState} from './boarding.state';

export const BOOKINGS_LOADED = 'BOOKINGS_LOADED';
export const BOOKING_STATE_CHANGED = 'BOOKING_STATE_CHANGED';

export var initialBoardingState: BoardingState = {
undoStack: [],
redoStack: [],
bookings: [],
message: "",
statistics: {
countBoarded: 0,
countBooked: 0,
countCheckedIn: 0
}
};

Then the module aligns boarding.reducer the reducer. It is just a function whose signature is the interface Reducer pretends. The type parameter T is to be replaced by the type that represents the affected subtree of the immutable state tree. Hence the following Reducer for use:

// boarding.reducer.ts (part 2)

export const boardingReducer: Reducer =
(state: BoardingState, action: Action) => {
switch (action.type) {
case BUCHUNGEN_LOADED: return bookingsLoaded (state,
action.payload);
case BUCHUNG_STATE_CHANGED: return buchungStateChanged (state,
action.payload);
default: return state;
}
}

Reducer

The reducer takes the current status of the type BoardingState as well as an action of the type Action and returns a new state that is also of type BoardingState is. Action consists of the properties type and payload. The type shows what action it is and uses one of the constants set up at the beginning. This information could be compared with the name of a function to be called. Meanwhile, the payload represents the transferred parameters in an object.

The action BOOKINGS_LOADED stores loaded bookings in the store. BOOKING_STATE_CHANGED however, accepts a booking with a changed state and updates the immutable state tree accordingly.

So that the reducer does not become confusing, it only consists of one switch, which triggers a suitable function depending on the transferred action type. The latter receives the current status and the payload. For the action BOOKINGS_LOADED the program performs the following function:

function bookingsLoaded (state: BoardingState, bookings):
BoardingState {

return {
bookings: bookings,
statistics: calcStatistic (bookings),
message: ""
};
}

It creates a new one using the postings transferred BoardingState. The helper method calculates the statistics for this retrieved array with postings calcStatistic (not shown here due to lack of space). It is necessary to create a new state object, as Redux relies on immutables (see Part 1). For this reason, changing the transferred status would not be permitted.

Several libraries help developers with such a venture. Examples of this are Facebook's Immutable.js or the seemingly handier seamless-immutable. In order to keep the complexity in check and at the same time to demonstrate the direct handling of immutables, the present text does not use such libraries.

To process the action BOOKING_STATE_CHANGED comes the function BookingStateChanged for use:

function bookingStateChanged (state: BoardingState, booking):
BoardingState {

var idx = state.bookings.findIndex (
b => b.flugID == booking.flugID
&& b.passierID == booking.passierID);

var newBookings = state.bookings.slice (0);
newBookings [idx] = booking;

return {
bookings: new bookings,
statistics: calcStatistic (new bookings),
message: ""
};
}

It accepts a booking and determines its position within the status. To do this, it uses the properties flightID and PassengerIDthat clearly identify a booking. Since the use of immutables prohibits changing an existing array, copied BookingStateChanged the array bookings with the JavaScript function slice. Replaced in the new array BookingStateChanged the booking concerned by its current version. Then it delivers a new one BoardingState with the changes and recalculated statistics.

For performance reasons and to make things easier, the new status refers to the unchanged bookings of its predecessor. This is a common practice that libraries and functional languages ​​also use for optimization. The only thing to note is that all changed nodes of the tree have to be recreated. A node is to be regarded as changed if it or one of its subordinate nodes has been modified.

Bootstrapping and Components

So that @ ngrx / store is available at runtime, it must be included in the global provider configuration of Angular 2 when the application is bootstrapped. Developers can use the provideStore that accepts two parameters: The first is an object with a reducer for each branch of the first hierarchical level of the State Tree. The properties of the object must have the names of the branches. The second parameter refers to the initial version of the entire state tree.

import {bootstrap} from 'angular2 / platform / browser';
import {AppComponent} from './app.component';
import {boardingReducer, initialBoardingState} from
'./boarding/boarding.reducer';
import {AppState} from './boarding/boarding.state';

var initAppState: AppState = {
boarding: initialBoardingState
}

var services;
services = [
[...]
provideStore ({boarding: boardingReducer}, initAppState)
];

bootstrap (AppComponent, services);

To interact with the store, the individual components and services can now have an instance of the class Store get injected. The type parameter T is to be replaced by the type of state tree. An example of this can be found in the next source code excerpt. This is the component BoardingComponentwho have a provider for a Boarding service (not shown here for reasons of space) for loading bookings. It also determines some pipes and the change detection strategy OnPushthat initiates the optimization process for immutables and observables.

The constructor of the BoardingComponent can also be Boarding service one Store inject. The method ngOnInit, which Angular 2 initiates when the component is initialized, then loads with the Boarding service all bookings of the flight 1. The latter is then deposited in the store by using the method dispatch the action BOOKINGS_LOADED and transfers the bookings as a payload. Additionally she relates with the method select an observable that informs about changes to the statistics.

The getter bookings analogously, retrieves an observable from the store that observes booking changes. The remaining getters register with the observable with the statistics and form the statistics object obtained from them map on other observables. They in turn present the individual values ​​of the statistics object.

The function changeState caused by calling dispatch performing the action BOOKING_STATE_CHANGED. To prevent the existing booking from having to be changed, use changeState The method Object.assign. It creates a new one using the transferred objects.

@Component ({
templateUrl: 'app / boarding / boarding.component.html',
providers: [BoardingService],
pipes: [BookingStatusPipe, BookingStatusColorPipe],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BoardingComponent {

statistics: Observable ;

constructor (private boardingService: BoardingService, private
store: Store ) {
}

ngOnInit () {
var that = this;
this.boardingService.find (1) .subscribe (
(bookings) => {
that.store.dispatch ({type: BOOKINGS_LOADED,
payload: bookings});
},
(err) => {console.debug (err); }
);

this.statistik = this.store.select (s => s.boarding.statistik);
}
get bookings () {
return this.store.select (s => s.boarding.bookings);
}

get countBoarded () {
return this.statistik.map (s => s.countBoarded);
}

get countBooked () {
return this.statistik.map (s => s.countBooked);
}

get countCheckedIn () {
return this.statistik.map (s => s.countCheckedIn);
}

public changeState (booking, state) {
if (booking. booking status == state) return;
var newBooking = Object.assign ({}, booking,
{booking status: state});
this.store.dispatch ({type: BUCHUNG_STATE_CHANGED,
payload: newBooking});
}
}

Use templates

The component's template is linked to the properties provided. The pipe comes to indicate that it should consume the supplied observables async used in the context of data binding expressions. It subscribes to the observable and propagates any changes it receives to the view. Angular 2 does not have to check of its own accord whether data has changed. This has a positive effect on performance.

<h2>Überblick</h2>

<table class="table table-striped">
<tr>
<th style="color:red">Gebucht</th>
<th style="color:orange">Checked in</th>
<th style="color:green">Boarded</th>
</tr>
<tr>
<td style="color:red">{{countBooked | async}}</td>
<td style="color:orange">{{countCheckedIn | async}}</td>
<td style="color:green">{{countBoarded | async}}</td>
</tr>
</table>

<h2>Details</h2>

<table class="table table-striped">

<tr>
<th>Vorname</th>
<th>Name</th>
<th>Aktueller Status</th>
<th>Neuer Status</th>
</tr>

<tr *ngFor="#b of buchungen | async">

<td>{{b.passagier.vorname}}</td>
<td>{{b.passagier.name}}</td>
<td [style.color]="b.buchungsStatus | buchungsStatusFarbe ">
{{b.booking status | booking status}}
<td>
<a style="cursor:hand" (click)="changeState(b,0)">
Booked |
<a style="cursor:hand" (click)="changeState(b, 1)">
Checked in |
<a style="cursor:hand" (click)="changeState(b, 2)">
Boarded
</td>
</tr>

</table>

Conclusion and outlook

The @ ngrx / store library supports developers in using the Redux pattern in Angular 2 and thus improves the maintainability of complex applications. It is reduced to the essentials and primarily offers a store implementation as well as specifications for reducers. In addition, it can work with the Angular 2 provider concept. Anyone who knows the approaches behind Redux should quickly get to grips with @ ngrx / store.

One challenge when using Redux is dealing with immutables. This is because JavaScript was not primarily designed for this and JS developers are not necessarily used to dealing with such structures. However, with a little practice and a few patterns, some of which were used in the code excerpts shown, the hurdle can be overcome. Libraries like Immutable.js or seamless-immutable also help developers.

Deep state trees harbor further difficulties. In the application under consideration, for example, a booking refers to a passenger. This means that the application is difficult for passengers to use independently of bookings. In addition, the use of immutables makes it necessary to change bookings when a passenger changes. The Redux community solves this by normalizing the state tree. After that, individual objects are no longer nested in one another, but exist independently of one another and refer to one another via IDs. This is comparable to the use of foreign keys in relational databases. Libraries like normalizr make work easier. (jul)

Manfred Steyer
is a freelance trainer and consultant with a focus on Angular 2 and service architectures. He writes for O'Reilly and heise Developer. In his current book "AngularJS: Modern Web Applications and Single Page Applications with JavaScript" he deals with the many pages of the popular JavaScript framework from the pen of Google.

Leave a Comment