Skip to main content

Retaining State of The Angular App When Page Refresh With NGRX

 Most of the web applications are written with SPA frameworks such as Angular, React, Vue.js, etc. The problem with these SPAs is that the single page is loaded in the browser once and then the framework will take care of all the routing among pages and gives the impression to the user that it is a multi-page application. When you refresh your page in the browser that single page called index.html is reloaded and you will lose the entire state of the application.

There are ways you can retain the state of the application in those cases. One way is to make the necessary API calls to the backend server and retrieve the data again. This works well if you have a small number of calls but, it strains the server. Another way is to design your application in such a way that when you put the data in the local storage and retain it as soon as the page is loaded in the browser.

In this post, we will see how we can retain the state of the application in Angular apps with the help of NGRX store and local storage.

  • Prerequisites
  • Example Project
  • Problem
  • Solution
  • Implementation
  • Demo
  • Summary
  • Conclusion

Prerequisites

There are some prerequisites for this article. You need to have nodejs installed on your laptop and how http works. If you want to practice and run this on your laptop you need to have these on your laptop.

This is going to be a big post if I included the whole implementation of the project. So, I created separate posts for the actual implementation of the project without the NGRX store and one with NGRX Store. If you are a beginner to the Angular you can have a look at the below post. Otherwise, you can skip to the next section. This post is about step by step guide on how to develop an Angular app with NodeJS backend.

How To Develop and Build Angular App With NodeJS

How To Implement NGRX Store In Angular App

Example Project

Let’s have a look at the example project. We have a simple app in which we can log in, signup, and add tasks, delete tasks, and edit tasks, etc. The entire application state is maintained with NGRX. We have actions, effects, reducers, etc. We will see all in this example.

Image for post
Example Project

Here is the example project in the GitHub where you can clone and run it on your local machine.

// clone the project
git clone https://github.com/bbachi/angular-ngrx-example.git
// Run the API
cd api
npm install
npm run dev
// Run the Angular App
cd ui
npm install
npm start

Problem

The problem we are facing is that when you load the Single Page Application in the browser you actually load the index.html with all the requiring libraries only once. From then on, the Angular framework kick in and loads the appropriate modules and pages as route changes. When you refresh the page you lose all the state of the application since you are reloading the index.html with all the required dependencies again.

Image for post
Example Scenario

For example, lets look at the above scenario with the example project provided in the article. We signed up and logged in. You can see the logged in user profile information on the right side of the header and created tasks as well. But, as soon as you refresh the page the entire state of the app is lost and you can’t see the logged in user information in the header and tasks.

Solution

As I said earlier we have so many options to maintain the state and we are seeing how we can preserve the sate with the help of NGRX store and local storage in this example.

Image for post
Retaining the state with NGRX Store and Local Storage

If you look at the above diagram, when you refresh the page or reload the page you are saving the application state in the local storage and retrieve it after the reload and populate the reducers. As long as the reducers are populated with the current state all the components subscribed to the store receive the data. This is what we call regydration of NGRX store. You can retain the state in this way. Let’s see the implementation step by step below.

Implementation

The implementation of the above solution is pretty straightforward. Make sure you follow the NGRX example article given above before this or click this link.

First we need to define separate file which handles all the storage related activities. It has the following operations such as getting current state, get item, get item by key, save state, save item, etc. You can define functions based on your requirement.


export const getThisState = (stateName) => {
  try{
      const serializedState = localStorage.getItem(stateName);
      if(serializedState === null){ return undefined }
      return JSON.parse(serializedState);
  }catch(err){
      return undefined
  }
}

export const getItem = (itemName) => {
  const items = getThisState(itemName)
  if (items === undefined) {
      return {todos : []}
  } else {
      return items
  }
}

export const saveItem = (key,data) => {
  const serializedState = JSON.stringify(data);
  localStorage.setItem(key,serializedState);
}

export const getItemByKey = (key) => {
  try{
      const serializedState = localStorage.getItem(key);
      if(serializedState === null){ return undefined }
      return JSON.parse(serializedState);
  }catch(err){
      return undefined
  }
}

export const deleteItemByKey = (key) => localStorage.setItem(key,null)

export const emptyLocalStorage = (reducerkeys) => {

  try{
      if(undefined != reducerkeys && reducerkeys.length > 0){
          reducerkeys.forEach(key => {
              localStorage.setItem(key,null);
          })
      }
  }catch(err){
      //console.log("ERROR===emptyLocalStorage==>>>")
  }
}

export const clearStorage = () => localStorage.clear();

Install the below library called ngrx-store-localstorage as a dependency with the following command

npm install ngrx-store-localstorage --save

Once you installed this one you have to register the reducers with it so that the library automatically syncs that particular reducers with the localstorage. You don’t have to do anything explicitly. Here is the index.ts file where you register all the reducers with the local storage with the help of ngrx-store-localstorage. Notice the lines 8, 23, 24,25. Since we have to preserve both user information and tasks information you have to register both reducers.

import {
  ActionReducer,
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import { localStorageSync } from 'ngrx-store-localstorage';
import { environment } from '../../environments/environment';
import * as fromUser from './reducers/user.reducer';
import * as fromTodo from './reducers/todo.reducer';

export interface State {
  user: fromUser.State;
  todo: fromTodo.State;
}

export const reducers: ActionReducerMap<State> = {
  user: fromUser.reducer,
  todo: fromTodo.reducer,
};

const reducerKeys = ['user', 'todo'];
export function localStorageSyncReducer(reducer: ActionReducer<any>): ActionReducer<any> {
  return localStorageSync({keys: reducerKeys})(reducer);
}

// console.log all actions
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
  return function(state, action) {
    console.log('state', state);
    console.log('action', action);

    return reducer(state, action);
  };
}


export const metaReducers: MetaReducer<State>[] = !environment.production ? [debug, localStorageSyncReducer] : [localStorageSyncReducer];

export const getLoginState = createFeatureSelector<fromUser.State>('user');

export const getLoggedInUser = createSelector(
  getLoginState,
  fromUser.getLoggedInUser
);

export const userLogin = createSelector(
  getLoginState,
  fromUser.userLogin
);

export const userSignup = createSelector(
  getLoginState,
  fromUser.userSignup
);


// Todo reducers Begin

export const geTodoState = createFeatureSelector<fromTodo.State>('todo');

export const getTasks = createSelector(
  geTodoState,
  fromTodo.getTasks
);

This part takes care of the automatically populate the state in the reducers to the localstorage with the key name as reducer names. Now, you have the data in the local storage how you can retrieve this and populate the reducers when the page refresh happens.

Let’s see how we can achieve that with help of stoarge.ts file that we defined above. We have initial state in each reducer as below you need to populate the initial state from storage by reading the appropriate keys as shown in the following reducer files. Notice the initial state of each reducer we are reading from the local storage/session storage.

import { Action, createReducer, on } from '@ngrx/store';
import { Task } from '../entity';
import * as todoActions from '../actions';
import * as _ from 'lodash'
import * as storage from '../state/storage';

export interface State {
  tasks?: Task[];
  currentTask?: Task;
  deleteTaskId?: any;
  result?: any;
  isLoading?: boolean;
  isLoadingSuccess?: boolean;
  isLoadingFailure?: boolean;
}

export const initialState: State = {
  tasks: storage.getItem('todo').tasks,
  currentTask: {},
  deleteTaskId: '',
  result: '',
  isLoading: false,
  isLoadingSuccess: false,
  isLoadingFailure: false
};

const todoReducer = createReducer(
  initialState,

  // GeTasks
  on(todoActions.getTasks, (state) => ({...state, isLoading: true})),
  on(todoActions.getTasksSuccess, (state, result) => ({tasks: result.response, isLoading: false, isLoadingSuccess: true})),

  // Create Task Reducers
  on(todoActions.createTask, (state, {task}) => ({...state, isLoading: true, currentTask: task})),
  on(todoActions.createTaskSuccess, (state, result) => {
    const tasks = undefined !== state.tasks ? _.cloneDeep(state.tasks) : [];
    const currentTask = undefined !== state.currentTask ? _.cloneDeep(state.currentTask) : {};
    currentTask.id = result.taskId;
    tasks.push(currentTask);
    return {
      tasks,
      isLoading: false,
      isLoadingSuccess: true
    };
  }),

  // Delete Task Reducers
  on(todoActions.deleteTask, (state, {taskid}) => ({...state, isLoading: true, deleteTaskId: taskid})),
  on(todoActions.deleteTaskSuccess, (state, result) => {
    let tasks = undefined !== state.tasks ? _.cloneDeep(state.tasks) : [];
    if (result.status) {
      tasks = tasks.filter(task => task.id !== state.deleteTaskId);
    }
    return {
      tasks,
      isLoading: false,
      isLoadingSuccess: true
    };
  }),

   // Edit Task Reducers
   on(todoActions.editTask, (state, {task}) => ({...state, isLoading: true, currentTask: task})),
   on(todoActions.editTaskSuccess, (state, result) => {
    let tasks = undefined !== state.tasks ? _.cloneDeep(state.tasks) : [];
    const currentTask = undefined !== state.currentTask ? _.cloneDeep(state.currentTask) : {};
    tasks = tasks.map(tsk => {
      if (tsk.id === currentTask.id) {
        tsk = currentTask;
      }
      return tsk;
    });
    return {
      tasks,
      isLoading: false,
      isLoadingSuccess: true
    };
  })
);

export function reducer(state: State | undefined, action: Action): any {
  return todoReducer(state, action);
}

export const getTasks = (state: State) => {
  return {
    tasks: state.tasks,
    isLoading: state.isLoading,
    isLoadingSuccess: state.isLoadingSuccess
  };
};

//
import { Action, createReducer, on } from '@ngrx/store';
import { User } from '../entity';
import * as userActions from '../actions';
import * as storage from '../state/storage';

export interface State {
  user: User;
  result: any;
  isLoading: boolean;
  isLoadingSuccess: boolean;
  isLoadingFailure: boolean;
}

export const initialState: State = {
  user: storage.getItem('user').user,
  result: '',
  isLoading: false,
  isLoadingSuccess: false,
  isLoadingFailure: false
};

const loginReducer = createReducer(
  initialState,
  on(userActions.login, (state, {user}) => ({user, isLoading: true})),
  on(userActions.loginSuccess, (state, result) => ({user: result.user, result, isLoading: false, isLoadingSuccess: true})),
  on(userActions.signup, (state, {user}) => ({user, isLoading: true})),
  on(userActions.signupSuccess, (state, result) => ({user: state.user, result, isLoading: false, isLoadingSuccess: true}))
);

export function reducer(state: State | undefined, action: Action): any {
  return loginReducer(state, action);
}

export const getLoggedInUser = (state: State) => {
  return {
    user: state.user,
    isLoadingSuccess: state.isLoadingSuccess
  }
};

export const userLogin = (state: State) => {
  return {
    user: state.user,
    result: state.result,
    isLoading: state.isLoading,
    isLoadingSuccess: state.isLoadingSuccess
  }
};

export const userSignup = (state: State) => {
  return {
    user: state.user,
    result: state.result,
    isLoading: state.isLoading,
    isLoadingSuccess: state.isLoadingSuccess
  }
};



Demo

We have implemented the solution, let’s see the demo we can see that we can preserve the state of the application once we login and create tasks.

Image for post
Demo

We can see the library ngrx-store-localstorage in action below. As soon as you add anything and send that it store it automaticllay puts that state in the local storage. As we add tasks the library automatically updates the local storage.

Image for post
Demo

Summary

  • The problem with these SPAs is that the single page is loaded in the browser once and then the framework will take care of all the routing among pages and gives the impression to the user that it is a multi-page application.
  • When you refresh your page in the browser that single page called index.html is reloaded and you will lose the entire state of the application.
  • You can clone the entire project from here.
  • The solution for the above problem is that when you refresh the page or reload the page you are saving the application state in the local storage and retrieve it after the reload and populate the reducers.
  • As long as the reducers are populated with the current state all the components subscribed to the store receive the data. This is what we call regydration of NGRX store.
  • You can define separate file which handles all the storage related activities. It has the following operations such as getting current state, get item, get item by key, save state, save item, etc.
  • Install the ibrary called ngrx-store-localstorage as a dependency with the this command npm install ngrx-store-localstorage --save
  • Once you installed this one you have to register the reducers with it so that the library automatically syncs that particular reducers with the localstorage. You don’t have to do anything explicitly.
  • We have initial state in each reducer as below you need to populate the initial state from storage by reading the appropriate keys


















































































































































Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

JavaScript new features in ES2019(ES10)

The 2019 edition of the ECMAScript specification has many new features. Among them, I will summarize the ones that seem most useful to me. First, you can run these examples in  node.js ≥12 . To Install Node.js 12 on Ubuntu-Debian-Mint you can do the following: $sudo apt update $sudo apt -y upgrade $sudo apt update $sudo apt -y install curl dirmngr apt-transport-https lsb-release ca-certificates $curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $sudo apt -y install nodejs Or, in  Chrome Version ≥72,  you can try those features in the developer console(Alt +j). Array.prototype.flat && Array.prototype. flatMap The  flat()  method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth. let array1 = ['a','b', [1, 2, 3]]; let array2 = array1.flat(); //['a', 'b', 1, 2, 3] We should also note that the method excludes gaps or empty elements in the array: let array1 ...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...