3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 11

Persisting Data in Local Storage

Making sure our data sticks around

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 use Ionic’s IonicStorageModule. We have already talked about this in a previous module, but the general idea is that Ionic provides us with a simple key/value storage system, and underneath the hood it can use various storage mechanisms to store that data. In the browser, it can store the data in the browsers local storage but, as we have discussed before, that can be unreliable for permanent data storage. If we intend to build this application natively for iOS and Android, then we can also make use of the native SQLite database for more reliable storage of data, and we can configure the IonicStorageModule to use that automatically if it is available.

Install Ionic Storage and the SQLite Plugin

To use the IonicStorageModule we will need to install the following package:

npm install @ionic/storage-angular

If we want to use an SQLite database when we run this application natively, then we will also need to install the following package:

npm install localforage-cordovasqlitedriver

Then, we can configure the storage mechanisms we want to use in our root module.

Add the following import to the root AppModule in app.component.ts:

    IonicStorageModule.forRoot({
      driverOrder: [
        // eslint-disable-next-line no-underscore-dangle
        CordovaSQLiteDriver._driver,
        Drivers.IndexedDB,
        Drivers.LocalStorage,
      ],
    }),

For reference, these are the imports you will need to add to the top of your file:

import { Drivers } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular';
import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';

What we have added to our AppModule gives us the ability to use the Ionic Storage API, and through the driverOrder allows us to specify our order of preference for storage mechanisms. We are saying we want to use the native SQLite database if it is available, but if it is not we will fall back to IndexedDB and then to Local Storage. If we are running in the browser, not natively on a device, then the IonicStorageModule will fall back to using either IndexedDB or LocalStorage depending on what is available.

Creating a storage service

We have the ability to use Ionic’s Storage API, now we are going to create a service to interact with it. Technically, we could just interact with storage through any service we want to store data in, but by creating a service specifically for storage we can make it easier to use in our application and all of our other services can rely on the storage service, instead of using the Ionic Storage API directly.

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

import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
import { from, Observable } from 'rxjs';
import { map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { Checklist } from '../interfaces/checklist';
import { ChecklistItem } from '../interfaces/checklist-item';

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  #checklistHasLoaded = false;
  #checklistItemsHasLoaded = false;

  storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));

  loadChecklists$ = this.storage$;

  loadChecklistItems$ = this.storage$;

  constructor(private ionicStorage: Storage) {}

  saveChecklists(checklists: Checklist[]) {}

  saveChecklistItems(checklistItems: ChecklistItem[]) {}
}

This service is a little complex, so I have left the methods unimplemented for now, and we will tackle them one at a time. First, let’s talk about the stream we have set up for storage$:

private storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));

In order to use Ionic Storage, we need to inject it, and call its create method. This returns a Promise that will resolve with an instance of the storage object that we can interact with to set/get data. Without using observable streams we might do something like this:

private storage: Storage;

async init(){
    this.storage = await this.ionicStorage.create();
}

Which requires a little more code, but the main annoying thing about this approach is that when we try to use this.storage we need to somehow check that the init method has finished running. We also need to make sure that we call init from somewhere in the application.

What we do instead is create a stream from this Promise by using the from creation operator:

private storage$ = from(this.ionicStorage.create())

And now if we want to use storage we can subscribe to this stream to get access to it. But… with the code above this would re-run this.ionicStorage.create() and create a new instance of storage every time we subscribe to storage$. This is not what we want, so we do this instead:

private storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));

By piping on shareReplay(1) it will run this.ionicStorage.create() the first time some code subscribes to this.storage$, but every time after that it will just share the existing result with new subscribers, rather than re-running the observable creation logic. The first subscriber triggers the storage to be created, and any subscribers after that just instantly get a reference to that same storage.

Now let’s implement the rest.

Modify the load* streams to reflect the following:

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).