Managing the client side state with help of NgRx(Redux)


  When we are working with rest environment it becomes a necessity to manage the user's state at the client side instead of making a call to the server every now and then.

Hey guys, in this post we are going see how to manage the state of the user at the client side in angular application with the help of NgRx.

[at the bottom of the post you will get the link to download the project]

Before starting, we need to understand the following terms of NgRx in short.
1.Store
2. Actions
3. Reducer
4. Effects (go here NgRx effects)



Store - as the name suggests it is the place where we keep user’s data.
Actions - with store we have data, but to manipulate the data, we need to define some actions that will be performed on data, like add or update action which will add or update the data.
Reducer - now we have data and actions as well but, we must implement the logic for a given action, means we need to write an implementation of add action.


Let’s do it step by step.

At very first, install the NgRx store which is the core module in NgRx.

Following is the installation command and description.
npm install –save @ngrx/store  

D:\Angular-DemoApps\Angular 6 - ngrx implementation> npm install --save @ngrx/store
npm WARN registry Using stale data from https://registry.npmjs.org/ because the host is inaccessible -- are you offline?
npm WARN registry Using stale package data from https://registry.npmjs.org/ due to a request error during revalidation.
npm WARN bootstrap@4.2.1 requires a peer of jquery@1.9.1 - 3 but none is installed. You must install peer dependencies yourself.
npm WARN bootstrap@4.2.1 requires a peer of popper.js@^1.14.6 but none is installed. You must
install peer dependencies yourself.
npm WARN @ngrx/store@8.2.0 requires a peer of @angular/core@^8.0.0 but none is installed. You
must install peer dependencies yourself.
npm WARN @ngrx/store@8.2.0 requires a peer of rxjs@^6.4.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted
{"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ @ngrx/store@8.2.0
updated 1 package in 21.757s

After completing the installation, we are going to create the actions and reducer.
Here is the user model which defines its properties.
export class User {
    idnumber;
    userNamestring;
    passwordstring;
    emailstring;
    contactNostring;
    addressstring;
    editedboolean;

    constructor() { };
}

After that, we are going to define, what actions can be performed with the user
User action page defines all the actions that can be performed on user data and the data needed to complete that action accepted as a constructor parameter.


user.actions.ts
import { Action } from '@ngrx/store';
import { User } from 'src/app/user/user';

export const ADD_USER = '[second page] add user';
export const DELETE_USER = '[second page] delete user';
export const UPDATE_USER = '[second page] update user';

export class AddUser implements Action {
    readonly type = ADD_USER;
    constructor(public payloadUser) { }
}
export class UpdateUser implements Action {
    readonly type = UPDATE_USER;
    constructor(public payload: { userUserindexnumber }) { }
}
export class DeleteUser implements Action {
    readonly type = DELETE_USER;
    constructor(public payloadnumber) { }
}

export type userListActions = | AddUser | DeleteUser | UpdateUser;


In action page we have defined add, delete, and update actions, which will respectively add delete and update user in client-side state(data).

We have defined actions, but we must need to specify how to perform the action on data so let’s implement their functionality, that’s where reducer comes into the picture. Reducer contains the business logic for the given action.


import * as UserActions from './user.actions';
import { User } from 'src/app/user/user';

export interface UserState {
    userListUser[];
}

const initialState = {
    userList: []
};

export function userActionReducer(state = initialStateactionUserActions.userListActions) {
    switch (action.type) {
        case UserActions.ADD_USER:
            return {
                ...state,
                userList: [...state.userListaction.payload]
            };

        case UserActions.DELETE_USER:
            return {
                ...state,
                userList: state.userList.filter((userindex=> {
                    return index != action.payload;
                })
            };

        case UserActions.UPDATE_USER:
            const oldUser = state.userList[action.payload.index];

            const updatedUser = {
                ...oldUser,
                ...action.payload.user
            };

            const updatedUserList = [...state.userList];
            updatedUserList[action.payload.index] = updatedUser;

            return {
                ...state,
                userList: updatedUserList
            };

        defaultreturn state;
    }
}

Here in the reducer we first declared a UserState that will be a type of the store and initialized the same, because if we don’t have the type how to validate and access the data from store, that’s why we have defined the type of state/store. Here we are dealing with the user’s data, so we have defined the type of the state as a list of users.

But this is all about user and user-related page but, our applications are never going to be that much small, we might have dozens or lot more than that type of data. To manage all that data in the state, we have one common state from all states at app level that is app reducer which will manage sub-states from app and map all reducers for sub-state-specific data.


app.reducer.ts
import { ActionReducerMap } from '@ngrx/store';

import * as fromUserActionsReducer from '../user-store/user.reducer';

export interface AppState {
    userListfromUserActionsReducer.UserState;
}

export const appReducerActionReducerMap<AppState> = {
    userList: fromUserActionsReducer.userActionReducer
};


Here we have declared the common state that is AppState and in app state, we have mapped the sub states and reducer to manage that state.

We are done with all prerequisite for ngrx state now is going to use it.

 a home page which will display the list of users exist in the application also having action buttons to add, delete and edit the user details.

homepage.component.html 
<div class="container">
    <div class="row no-gutters">
        <div class="col-2"></div>
        <div class="col-8">
            <label class="h3">Welcome to ngrx demo</label><br>
        </div>
        <div class="col-2"></div>
        <div class="col-2"></div>
        <div class="col-8">
            <table class="table table-bordered table-striped">
                <thead>
                    <tr>
                        <th colspan="4">
                            Existing Users
                        </th>
                        <th>
                            <button class="btn btn-info ml-2" (click)="goToRegisteration()">Add User</button>
                        </th>
                    </tr>
                    <tr>
                        <th>
                            Action
                        </th>
                        <th>
                            User name
                        </th>
                        <th>
                            Email
                        </th>
                        <th>
                            Contact No
                        </th>
                        <th>
                            Address
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <tr *ngFor="let userToShow of (localUserList | async).userList; let i = index">
                        <td>
                            <button class="btn btn-sm btn-info" (click)="editUser(i)">Edit</button>
                            <button class="btn btn-sm btn-danger ml-2" (click)="removeUser(i)">Delete</button>
                        </td>
                        <td>{{userToShow.userName}}</td>
                        <td>{{userToShow.email}}</td>
                        <td>{{userToShow.contactNo}}</td>
                        <td>{{userToShow.address}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
        <div class="col-2"></div>
    </div>
</div>

homepage.component.ts
import { ComponentOnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../user/user';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';

import * as UserActions from "../user-store/user.actions";
import * as fromApp from '../store/app.reducer'

@Component({
  selector: 'app-homepage',
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
  localUserListObservable<{ userListUser[] }>;

  constructor(
    private routerRouter,
    private storeStore<fromApp.AppState>
  ) { }

  ngOnInit() {
    this.localUserList = this.store.select('userList');
  }

  goToRegisteration() {
    this.router.navigate(['/secondpage']);
  }

  removeUser(indexnumber) {
    this.store.dispatch(new UserActions.DeleteUser(index));
  }

  editUser(indexnumber) {
    this.router.navigate([`/secondpage/${index}`]);
  }
}

On homepage we have created a table which will hold a list of added users, also we have kept few buttons to add, edit and delete, etc the user.
At first, we read if there is any user in the state(store) and displayed same on the home page, after that on add and edit button we redirected user to the second page so he can add/edit user, then on delete we have dispatched deleted user action on the store.

The second page will contain code to add a new user to the state. Here it is.
second-page.components.html
<div class="container">
  <div class="row no-gutters">
    <div class="col-2">
    </div>
    <div class="col-6">
      <form action="" #frm="ngForm">
        <table class="table">
          <thead>
            <tr>
              <th>
                <label class="h3">Registration</label>
              </th>
              <th>
                <button class="btn btn-info float-right" (click)="goToHome()">Back</button>
              </th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>User name</td>
              <td><input [(ngModel)]="user.userName" class="form-control" type="text" name="userName"></td>
            </tr>
            <tr>
              <td>password</td>
              <td><input [(ngModel)]="user.password" class="form-control" type="password" name="password"></td>
            </tr>
            <tr>
              <td>confirm password</td>
              <td><input class="form-control" type="password" name="password"></td>
            </tr>
            <tr>
              <td>email</td>
              <td><input [(ngModel)]="user.email" class="form-control" type="text" name="email"></td>
            </tr>
            <tr>
              <td>contact No</td>
              <td><input [(ngModel)]="user.contactNo" class="form-control" type="text" name="contactNo"></td>
            </tr>
            <tr>
              <td>address</td>
              <td><input [(ngModel)]="user.address" class="form-control" type="text" name="address"></td>
            </tr>
            <tr>
              <td></td>
              <td><button class="btn btn-light">Cancel</button>&nbsp;<button class="btn btn-primary"
                  (click)="saveUser()">save</button></td>
            </tr>
          </tbody>
        </table>
      </form>
    </div>
  </div>
</div>

second-page.component.ts
import { ComponentOnInit } from '@angular/core';
import { User } from '../user/user';
import { Store } from "@ngrx/store";
import { Observable } from 'rxjs';
import { RouterActivatedRoute } from '@angular/router';

import * as UserActions from "../user-store/user.actions";
import * as fromApp from '../store/app.reducer';

@Component({
  selector: 'app-second-page',
  templateUrl: './second-page.component.html',
  styleUrls: ['./second-page.component.css']
})
export class SecondPageComponent implements OnInit {
  userUser;
  localUserListObservable<{ userListUser[] }>;
  userIndexnumber = -1;

  constructor(
    private routerRouter,
    private activatedRouteActivatedRoute,
    private storeStore<fromApp.AppState>
  ) { }

  ngOnInit() {
    this.activatedRoute.params.subscribe(paramMap => {
      this.userIndex = +paramMap["index"];
      if (this.userIndex != undefined && this.userIndex > -1) {
        this.store.select("userList").subscribe(storeMap => {
          this.user = storeMap.userList[this.userIndex];
        });
      } else {
        this.user = new User();
      }
    })

  }

  saveUser() {
    let tmpUser = Object.assign({}, this.user);
    if (this.userIndex != undefined && this.userIndex > -1) {
      this.store.dispatch(new UserActions.UpdateUser({ user: tmpUserindex: this.userIndex }));
    } else {
      this.store.dispatch(new UserActions.AddUser(tmpUser));
    }
    this.goToHome();
  }

  goToHome() {
    this.router.navigate(['/home']);
  }

}

In the second page we have designed a form to accept the basic details of the user.
Here we have used the same page to edit as well as save new user, to achieve that we simply accepted the index of user list from home page in case of edit otherwise it will be the new user.
And finally, on save if it is edited user, we have dispatched update user action on store otherwise add user action.

Here is the output





And that’s it,
We have successfully managed the state of the user at the client-side with the help of ngrx.

I hope you like it.

[We will implement effects next post]

Here is the link to download the project [Download the project]
[ If you are preparing for an interview please click here - angular interview questions and answers ]

Comments