Reactive Programming is a must have for modern complex applications these days.
What is Reactive Programming?
Reactive Programming is an approach of event handling and data flow in applications. Using the Reactive Programming approach, application components and other objects are written to react to the data changes, instead of waiting for receiving these events.
This means that when a component is changing data, some other component will immediately react on the data changes. This brings in a programmable shift in building modern applications. Typically, such an approach is needed in more complex front-end applications.
The Angular framework makes use of RxJs for reactive programming. The RxJs provides an Observable for receiving and managing the received data from an external system. The Observable streams data to the receiving component.
In case of complex JavaScript applications, we need an Object-Model which will provide a state container for managing an Application State. This is where Redux comes into picture.
Redux is an open-source JavaScript library for managing application state and is most commonly used in React and Angular applications. Redux provides the following features:
- Store - is the state of the application. The Store stores the application state. The Store can be commonly available to all components of the application and can be modified when actions are dispatched to it.
- Action - are payloads of information. They send data from the application to store. The Store can be updated based on the payloads of actions.
- Reducers - are the functions that have knowledge of what to do for a given action. The application state can be changed by reducer by dispatching action to Store. The previous state of the Store is modified by the reducer using pure function.
- Dispatcher - are an entry point to dispatch the action.
NgRx in Angular Applications
In case of complex applications, there are multiple components that want to share data. Creating a global object for sharing across components is challenging.
That’s where NgRx comes into picture.
The Store and Unidirectional data flow in Ngrx are helpful in reducing tight coupling between components of an application. This loose coupling further reduces complexity of the application. Using Ngrx, the application state can be stored in one place which allows to have a global accessible state across all components of the application.
Figure 1 explains the behavior of an application when using Ngrx.
Figure 1: The behavior of Application when using Ngrx
NgRx in Angular - The Implementation
We will use Angular CLI for the application implementation. I am using Visual Studio Code (VSCode) as the Editor which can be downloaded from this link.
Editorial Note: If you are new to Angular CLI, check this www.dotnetcurry.com/angularjs/1409/angular-cli-tutorial.
Step 1: Create a new folder with the name ngrxdemo and open this folder in VSCode. Open the Node.js command prompt and navigate to the ngrxdemo folder.
To install Angular CLI, run the following command from the command prompt:
npm install -g @angular/cli
Once this command is executed successfully, run the following command to create a new project:
ng new ngrxapp
This will create a new Angular CLI application in the ngrxapp folder. Run the following command to navigate to this folder:
cd ngrxapp
To install the ngrx store, run the following command
Npm install --save-dev @ngrx/store
So now that we have the Ngrx Store with us, we can use it for our application.
Step 2: In the application, we have a src folder and this folder contains an app subfolder in it. Add a new folder in the app folder and name it as models. In this models folder, add a new file and name it as app.person.model.ts. Add the following code to this file.
export class Person {
constructor(public Id: number, public Name: string) {}
}
Listing 1: The Person class
Listing 1 shows a Person class. We will use this class to define schema for the Person information using Id and Name properties.
Step 3: In the app folder, add a new folder called actions. In this folder, add a new file and name it as app.person.actions.ts. Add the following code to this class:
import {Action} from '@ngrx/store';
import {Person} from './../models/app.person.model';
// 1. defining type of actions
export const ADD_PERSON = '[PERSON] Add';
export const REMOVE_PERSON = '[PERSON] Remove';
// 2. The AddPerson class
export class AddPerson implements Action {
readonly type = ADD_PERSON;
constructor(
public payload: Person
) {}
}
// 3. The RemovePerson class
export class RemovePerson implements Action {
readonly type = REMOVE_PERSON;
constructor(
public payload: Person
) {}
}
// 4. export all action classes so that it can be used
// by reducers
export type Actions = AddPerson | RemovePerson;
Listing 2: Actions with Action classes
Listing 2 does the following (Note: The numbers matches with comment numbers applied on the code):
1. This define action types. The ADD_PERSON and REMOVE_PERSON and the action type constants will be defined for performing add and remove actions.
2. The AddPerson class is defined with a parameterized constructor. This constructor accepts the Person object, this means that the Person object will be added to the state that is managed by the Store using Reducer. The read-only type defined by AddPerson class will be used by reducer to perform corresponding operations on the payload.
3. The RemovePerson class is defined with parameterized constructor. This constructor accepts the Person Object, this means that the Person object will be removed from the state that is managed by the Store using Reducer. The read-only type defined by RemovePerson class will be used by reducer to perform corresponding operations on the payload.
4. This step exports AddPerson and RemovePerson as Actions so that reducer can use it further.
Step 4: In the app folder, add a new folder and name it as reducers. In this folder, add a new file and name it as app.person.reducer.ts. In this file add the following code to it:
// 1. Import the Person object and all Actions
import {Person} from '../models/app.person.model';
import * as PersonActions from '../actions/app.person.actions';
// 2. defining the reducer with Initial State is blank
export function reducer(state: Person[] = [], action: PersonActions.Actions) {
switch (action.type) {
case PersonActions.ADD_PERSON:
return [...state, action.payload];
case PersonActions.REMOVE_PERSON:
state.forEach((p, index) => {
if (p.Id === action.payload.Id) {
state.splice(index, 1);
}
});
return state;
default:
return state;
}
}
Listing 3: Reducer
Listing 3 does the following (Note: The numbers match with numbers applied on the comments):
1. This step imports a Person object and all actions defined in app.person.actions.ts file.
2. The reducer() is a pure JavaScript function. This function accepts state object of type Person array and returns state with modifications in the Person array. The initial value for the state is defined as blank Person array. The second parameter is for the PersonActions. This parameter will be used to access Action types e.g. ADD_PERSON so that corresponding operations can be performed. The reducer() method uses switch..case and adds a new Person record in state (note that the spread operator is used here) for case ADD_PERSON, and removes a Person from state for the case REMOVE_PERSON.
Step 5: In the app folder, add a new file and name it as app.state.ts. Add the following code in this file:
import { Person } from './models/app.person.model';
// 1. this will define the app state
export interface AppState {
readonly persons: Person[];
}
Listing 4: The Application State declaration
Listing 4 defines the Application state. The AppState interface defines the Person array. This state will be used as the Application state for the application.
Step 6: Run the following commands in the Command Prompt to add new components in the application
ng g component ReadPerson
ng g component WritePerson
This will add read-person and write-person folder in the app folder. These folders contain component class, html file, style and spec files.
Step 7: Modify the app.module.ts file for defining store at the Application Module level:
// 1a. import reducer
import { reducer } from './reducers/app.person.reducer';
// 1b. import StoreModule
import { StoreModule } from '@ngrx/store';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {FormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import { ReadPersonComponent } from './read-person/read-person.component';
import { WritePersonComponent } from './write-person/write-person.component';
@NgModule({
declarations: [
AppComponent,
ReadPersonComponent,
WritePersonComponent
],
imports: [
BrowserModule, FormsModule, // 2. defining the reducer for Store
StoreModule.forRoot({
person : reducer
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Listing 5: The AppModule class defining the StoreModule at root level
Listing 5 does the following:
1. Importing required packages
- a. This imports reducer which is used to modify state of the store
- b. This imports StoreModule, which is used to access reducer at root level so that it can be injected across all components.
2. This step is used to register the reducer at root level using StoreModule.forRoot() method.
Step 8: Open write-person.component.ts from the write-person folder and modify the code as shown in the following listing :
import { Component, OnInit } from '@angular/core';
// 1a. Import Store
import { Store } from '@ngrx/store';
// 1b. Import App State
import { AppState } from './../app.state';
// 1c. Import Person object
import { Person } from './../models/app.person.model';
// 1d. Import Actions
import * as PersonActions from './../actions/app.person.actions';
import {Observable} from 'rxjs';
@Component({
selector: 'app-write-person',
templateUrl: './write-person.component.html'
})
export class WritePersonComponent implements OnInit {
person: Person;
// 2. Inject Store for AppState in Constructor
constructor(private store: Store<AppState>) {
this.person = new Person(0,'');
}
ngOnInit() {
}
// 3. dispatch the AddPerson Action
addPerson() {
this.store.dispatch(
new PersonActions.AddPerson(this.person)
);
}
clearPerson() {
this.person = new Person(0, '');
}
}
Listing 6: WritePersonComponent
The above code has following specifications (Note: Following line numbers matches with the comment numbers applied on the code)
1. Importing required packages
- a. This imports the Store object so that it can be injected in the component
- b. The imports AppState which is an Application State to be shared across all components
- c. Import Person object. This is used for adding a person in the State
- d. Import all actions so that the action can be dispatched for performing operations on the Store.
2. This injects the Store that is typed to AppState in the constructor of the Component. This step makes sure that the store is available throughout the lifecycle of the component.
3. The dispatch method of the store object is called. This is used to dispatch the AddPerson action. The Person object is passed to the AddPerson action.
Step 9: In the write-person.component.html, add the following Html code
<div class="container">
<table class="table table-bordered table-striped">
<tr>
<td>
Id
</td>
<td>
<input type="text" class="form-control" [(ngModel)]="person.Id">
</td>
</tr>
<tr>
<td>
Name
</td>
<td>
<input type="text" class="form-control" [(ngModel)]="person.Name">
</td>
</tr>
<tr>
<td>
<input type="button" value="New" class="btn btn-default" (click)="clearPerson()">
</td>
<td>
<input type="button" value="Save" class="btn btn-success" (click)="addPerson()">
</td>
</tr>
</table>
</div>
Listing 7: The Html view for WritePersonComponent
The Html code in Listing 7 uses two-way databinding of Person properties with input elements and the button uses event binding to call addPerson() method. The view in listing 7 is used to add Person object using the WritePersonComponent to the Store by dispatching the action.
Step 10: Write the following code in the read-person.component.ts
import { Component, OnInit } from "@angular/core";
// 1a. observable for storing Person information
import { Observable} from 'rxjs';
// 1b. Store, this will be injected in the constructor
import { Store } from "@ngrx/store";
// 1c. Person object
import { Person } from "./../models/app.person.model";
// 1d. The AppState to access Application state
import { AppState } from "./../app.state";
// 1e. Actions
import * as PersonActions from './../actions/app.person.actions';
@Component({
selector: "app-read-person",
templateUrl: "./read-person.component.html"
})
export class ReadPersonComponent implements OnInit {
// 2a. Person Observable
persons: Observable<Person[]>;
// 2b.The constructor in injected with Store for Application State
constructor(private store: Store<AppState>) {
this.persons = store.select('person');
}
// 3
deletePerson(p: Person) {
this.store.dispatch(new PersonActions.RemovePerson(p));
}
ngOnInit() {}
}
Listing 8: ReadPerson Component
The above code has following specifications (Note: Following line numbers matches with the comment numbers applied on the code)
1. The required imports for the component
- a. Import Observable which will be used to define Person Observable which will receive the Person data from the Store of the Application state.
- b. Import the Store
- c. Import the Person object for defining Observable of Person
- d. Import the AppState so that data can be available to this component
- e. Import all actions
2. Component code
- a. Define Observable of the Person Array so that the data from Store can be locally stored for the component.
- b. The Store types to AppState is injected in to the constructor so that the store can be available throughout the component’s lifecycle. The person data from the store is accessed using the select() method of the store. This method accepts the string that represents the reference of the data stored on the Store. In our case, it is the Person observable.
3. This is the method to delete the person by dispatching the RemovePerson action using dispatch() method of the Store.
Step 11: Modify the Html code of read-person.component.html as shown in Listing 9.
<div class="right" *ngIf="persons">
<h2>Person List</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let p of persons | async" >
<td>{{p.Id}}</td>
<td>{{p.Name}}</td>
<td>
<input type="button" value="Delete" class="btn btn-warning" (click)="deletePerson(p)">
</td>
</tr>
</tbody>
</table>
</div>
Listing 9: ReadPersonComponent View
The Html code in Listing contains a table. This table will load the Person data from the persons observable defined in the ReadPersonComponent. The *ngFor uses the async filter so that when the data is loaded in the persons observable from the Store, it will be used to render the Html table. The table also generates the button which uses event binding to delete the person from the store using action dispatch for the RemovePerson action.
Step 12: Modify the app.component.html in the app folder as shown in the following listing:
<app-write-person></app-write-person>
<app-read-person></app-read-person>
This will render WritePersonComponent and ReadPersonComponent when application is executed.
To execute then application, run the following command from the Command prompt
ng serve -o
This will open the browser and application will be rendered as shown in Figure 2.
Figure 2: The View in Browser showing both components
Enter Id and Name in the input boxes and click on the Save button, the data will be displayed in table as shown in Figure 3.
Figure 3: View in browser showing show data from Store can be displayed in Html Table
Since the Store is available at global level, the application state is maintained. This is the reason that the record added by one component, is available for another component.
When the Delete button is clicked on the Table row, the record will be removed.
Conclusion:
As we saw in the tutorial, the Ngrx store is a suitable way to handle application state across Angular objects.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Mahesh Sabnis is a DotNetCurry author and a Microsoft MVP having over two decades of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions), and Front-end technologies like Angular and React. Follow him on twitter @
maheshdotnet or connect with him on
LinkedIn