Persist data using Local Storage and Angular

Posted by

Intro

Local storage and Session storage are part of the so called Web storage. Both of them give us the possibility to store key-value data on client side. The data stored in local storage is deleted only when the user clear his cache or we decide to clear the storage. The less used session storage is cleared immediately when the web browser tab is closed. As a more often used storage, we will explore the local storage, his functionalities and how to use it in the context of Angular.

Use cases

You may be thinking now, when we could take advantage of this storage and why we need it – why we don’t just use a database like PostgreSQL ? There is a lot of use cases, but we will see two of them now.

  • Swagger – it’s a widely used tool to define REST APIs. If you are not familiar with it, go and check their website – https://swagger.io/ . One of their functionalities is Swagger Editor https://editor.swagger.io/ Allowing us to define APIs and immediately view the result of our definition, swagger editor is a very good example how to take advantage of the local storage. Every time the user edit the definition, it’s automatically persisted. This way even after you close the tab and get back later(event a few months) you will be able to see your changes there. You can see the persisted data opening the Webmaster Tools(F12 for Windows), go to Application and then – Local Storage. Try to change the definition and you will see your changes immediately stored there. Keep in mind that if you clear the browser cache, the persisted data will be loosed.
  • Another good example of a local storage usage is when you want to reduce the amount of HTTP calls for a data which is not often changed. Check if you have the wanted data in the storage – if it’s there just use it. Otherwise, do the call, fetch it and persist it. The next time, you can use it from the local storage rather than making one more call.

Pseudo code:

let data = localStorage.get('userData');

if (data) {
  return data;
} else {
  fetchData(); 
}

Now, when we are familiar with some of the use cases, let’s see how we could use the local storage in Angular.

Angular Context

Don’t get me wrong, there is nothing Angular specific in using the local storage. All the stuff we will implement can be reused in other frameworks or even in vanilla JavaScript. However, due to the things that can go wrong, like:

  • browser doesn’t support local storage
  • passing object will result in value [object Object], rather than actual object

it’s a good idea to create a service, which will be used as a communication channel between our application and the browser storage. We will only take care of the browser compatibility and stringifying there.

We will start with the basics, let’s create a new project using the Angular CLI

ng new angular-local-storage

Create a service using

ng g service local-storage

Our service will be a simple one, but can be extended later if more functionalities are needed. Here is the code for it(local-storage.service.ts)

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LocalStorageService {
  localStorage: Storage;

  constructor() {
    this.localStorage = window.localStorage;
  }

  get(key: string): any {
    if (this.isLocalStorageSupported) {
      return JSON.parse(this.localStorage.getItem(key));
    }

    return null;
  }

  set(key: string, value: any): boolean {
    if (this.isLocalStorageSupported) {
      this.localStorage.setItem(key, JSON.stringify(value));

      return true;
    }

    return false;
  }

  remove(key: string): boolean {
    if (this.isLocalStorageSupported) {
      this.localStorage.removeItem(key);

      return true;
    }

    return false;
  }

  get isLocalStorageSupported(): boolean {
    return !!this.localStorage
  }
}

One by one. As you can see, we are having a new class property – localStorage, which will keep the reference to the local storage. Rather than accessing it using window.localStorage, we will use this.localStorage in our methods. Check the service constructor – this is where we get the reference.

I’ve also created a ‘getter’ – get isLocalStorageSupported(), which we will use to check if the local storage is supported by the browser. As we already know, although it’s widely supported, some of the older browser versions(for example IE7) doesn’t supports web storage. You can check by yourself using the popular caniuse – https://caniuse.com/#search=localstorage

Now, the main functionality – set, remove, get. The common between these methods is that all of them firstly check if the localStorage is supported. This is very important, otherwise we will try to modify the storage without being supported and of course errors will be thrown, i.e. the functionalities will be broken.

The set and get methods are also doing an extra step – parsing and stringify. As already mentioned, this is going to fix an unexpected behavior when setting object values and retrieving them.

And lastly, keep in mind that set and remove return boolean type. It’s a good idea as we will always know if the methods are executed successfully. If for some reason – not(currently only browser compatibility), we will be aware of this.

As our basic local-storage service is in place, let’s test it. Go to app.component.ts and inject it.

  constructor(private localStorageService: LocalStorageService) {}

Then, create a public method called persist(or something else, it’s up to you).

  persist(key: string, value: any) {
    this.localStorageService.set(key, value);
  }

Also adjust the app.component.html, remove everything and add this.

<input type="text" (input)="persist('test', $event.target.value)">

It’s a simple input field which will call the persist method when someone change the value. Start the application and test. Type sometime and check if it’s persisted(you already know how).

Tadaaa … our service is working! Congrats!

Bonus

Let’s extend the service a bit. What we want to achieve is to be able to subscribe to the changes and react on them from other places of our application. We will achieve this using Subject, a special type of Observable which is Observable and Observer at the same time.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class LocalStorageService {
  localStorage: Storage;

  changes$ = new Subject();

  constructor() {
    this.localStorage   = window.localStorage;
  }

  get(key: string): any {
    if (this.isLocalStorageSupported) {
      return JSON.parse(this.localStorage.getItem(key));
    }

    return null;
  }

  set(key: string, value: any): boolean {
    if (this.isLocalStorageSupported) {
      this.localStorage.setItem(key, JSON.stringify(value));
      this.changes$.next({
        type: 'set',
        key,
        value
      });
      return true;
    }

    return false;
  }

  remove(key: string): boolean {
    if (this.isLocalStorageSupported) {
      this.localStorage.removeItem(key);
      this.changes$.next({
        type: 'remove',
        key
      });
      return true;
    }

    return false;
  }

  get isLocalStorageSupported(): boolean {
    return !!this.localStorage
  }
}

This is the adjusted service. The only new stuff here is that the Subject with name “changes$” will emit on both set and remove actions(get is not need as nothing new happens with the data). We will emit the type of action, the key which will manipulate and in case of set – value, in case of remove – nothing.

Now, every component which has access and can inject the service is able to track the changes. Let’s do a quick test. Got to app.component.ts and create a property which will reference the local storage changes. Like this:

import { Component } from '@angular/core';
import { LocalStorageService } from './local-storage.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  localStorageChanges$ = this.localStorageService.changes$;

  constructor(private localStorageService: LocalStorageService) {}
  persist(key: string, value: any) {
    this.localStorageService.set(key, value);
  }
}

And render them in app.component.html

<input type="text" (input)="persist('test', $event.target.value)">

<div *ngIf=" localStorageChanges$ | async as localStorageChanges">
  {{ localStorageChanges.type }}
  {{ localStorageChanges.key }}
  {{ localStorageChanges.value }}
</div>

Start the app and start changing the input, you will see the latest changes rendered.

You can find the final version at Github –
https://github.com/vikobg/first-class-js/tree/master/angular-local-storage

Good luck!