3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 11

Persisting Data in Local Storage

STANDARD

Persisting Data in Local Storage

We have our core functionality working now, but as soon as we refresh the application we lose all of our data. This lesson is going to be about making sure that data sticks around.

There are different ways you can go about persisting data and state. In some cases, your application might use an external database (especially if this is an application where data is shared by users like a chat application or social network). However, in this case, the data is going to be stored completely locally on the device itself.

To achieve this, we are going to make use of the Local Storage API. This is a simple type of key/value storage that is available in the browser by default. It is by no means the best place to store data — storage space is limited and there is the potential for the stored data to be lost. For important data that you don’t want disappearing on you, I would definitely recommend against only storing data in local storage. In later application walkthroughs we look at using an actual remote backend for data storage, but for now local storage will suit our purposes fine.

Creating a Storage Service

We are going to create a service to handle storage for us, but before we implement the storage mechanism itself we are going to discuss the concept of creating our own InjectionToken.

Create a file at src/app/shared/data-access/storage.service.ts and add the following:

import { Injectable, InjectionToken, PLATFORM_ID, inject } from '@angular/core';

export const LOCAL_STORAGE = new InjectionToken<Storage>(
  'window local storage object',
  {
    providedIn: 'root',
    factory: () => {
      return inject(PLATFORM_ID) === 'browser'
        ? window.localStorage
        : ({} as Storage);
    },
  }
);

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  storage = inject(LOCAL_STORAGE);
}

This is another one of those things where it looks like we are just complicating things. In order to interact with local storage, we want to use:

window.localStorage

So… why all this other junk? Technically we don’t need this for our purposes, but we are creating a safer design for our Angular application.

We are directly accessing a browser API here by using window — but an Angular application does not necessarily always run in the context of a browser, if you were using SSR (server side rendering) for example your Angular application would not have access to the window object.

If you know your application will only ever run in the browser, and you want to just ignore all this stuff, you can do that if you like — you can just use window.localStorage directly. But this is still a useful technique in general.

We might want to create a custom InjectionToken when we want something to change based on some kind of condition. For example, in this case we are changing what our injected LOCAL_STORAGE is (depending on the browser environment). In another circumstance, we might want to change what an injected dependency is based on whether the application is running a development or production version.

The basic idea is that we create an InjectionToken like this:

export const LOCAL_STORAGE = new InjectionToken<Storage>(
  'window local storage object',
  {
    providedIn: 'root',
    factory: () => {
      return inject(PLATFORM_ID) === 'browser'
        ? window.localStorage
        : ({} as Storage);
    },
  }
);

Just like with a normal service, we can either provide it in root or we can manually provide it to wherever we want to use it.

Then we have a factory function that determines what will actually be injected when we run inject(LOCAL_STORAGE). In this case, we check if we are running in the browser — in which case we will use window.localStorage. Otherwise, we can supply our alternate storage mechanism. We are not actually using an alternate storage mechanism here, we are just providing a fake object that will satisfy the Storage type.

Before we move on, here is another injection token from a later application build:

export const AUTH = new InjectionToken('Firebase auth', {
  providedIn: 'root',
  factory: () => {
    const auth = getAuth();
    if (environment.useEmulators) {
      connectAuthEmulator(auth, 'http://localhost:9099', {
        disableWarnings: true,
      });
    }
    return auth;
  },
});

This one deals with setting up authentication for Firebase. If a development version of the application is running, it will connect to the local emulator. If a production version of the application is running, it will connect to the actual Firebase service.

This is an easily overlooked but very powerful tool.

With our injection token in place, we can implement the rest of our service.

Implement the rest of the StorageService:

import { Injectable, InjectionToken, PLATFORM_ID, inject } from '@angular/core';
import { of } from 'rxjs';
import { Checklist } from '../interfaces/checklist';
import { ChecklistItem } from '../interfaces/checklist-item';

export const LOCAL_STORAGE = new InjectionToken<Storage>(
  'window local storage object',
  {
    providedIn: 'root',
    factory: () => {
      return inject(PLATFORM_ID) === 'browser'
        ? window.localStorage
        : ({} as Storage);
    },
  }
);

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  storage = inject(LOCAL_STORAGE);

  loadChecklists() {
    const checklists = this.storage.getItem('checklists');
    return of(checklists ? (JSON.parse(checklists) as Checklist[]) : []);
  }

  loadChecklistItems() {
    const checklistsItems = this.storage.getItem('checklistItems');
    return of(
      checklistsItems ? (JSON.parse(checklistsItems) as ChecklistItem[]) : []
    );
  }

  saveChecklists(checklists: Checklist[]) {
    this.storage.setItem('checklists', JSON.stringify(checklists));
  }

  saveChecklistItems(checklistItems: ChecklistItem[]) {
    this.storage.setItem('checklistItems', JSON.stringify(checklistItems));
  }
}
STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).