Add Persistence to PWA with IndexedDB

Web applications have multiple options for storing user information locally and these methods vary greatly in capacity and behavior. The Indexed Database API, commonly referred to as indexedDB, is a NoSQL database and supported by all major browsers. In today’s tutorial we will implement a simple interface for using indexedDB, which you can then add to a progressive web application (PWA) to create an offline storage.

Pros and Cons

Before we get into the weeds of implementation, let us first investigate the reasons for choosing indexedDB.

Pros

  • Allows adding offline storage to web applications
  • Very liberal storage limit
  • NoSQL implementation fits many use cases and works well with JSON data
  • Can be used to store multimedia like images and video (1
  • Good libraries exist to make implementation quick
  • Asynchronous API

Cons

  • Must migrate when changes occur in schema
  • Storage is local and synchronization is necessary between devices

1) Check per browser support for blob objects; if not supported you must store media in base64 format.

Based on your application needs, indexedDB may be a good fit for you, but it comes with certain assumptions. You get a large storage for storing complex structures, but will have to deal with schema migrations and more implementation complexity. For very simple use cases you might want to consider localStorage, sessionStorage or cookies instead.

Requirements

  1. Node.js   install here ↗
  2. Any javascript IDE of your choice   examples ↗

Implementation

In this tutorial we will build a simple javascript (ES6) database class using indexedDB. We will use Dexie.js, which is a library that makes working with indexedDB very simple.

We assume you are adding a database to an existing project. However, if you are starting this tutorial without any existing project, first create a project directory, then run npm init to initialize a new project.

1. Add dexie as a dependency
1
npm install dexie

We will use dexie as a way to define our database schema and to execute various operations on the database.

2. Create db.js file

This file will be our database implementation

3. Import dexie

In db.js, import dexie as a dependency:

1
import Dexie from 'dexie';
4. Declare the Database class
1
2
3
4
5
6
7
import Dexie from 'dexie';

export default class Database {

// our implementation will go here...

}
5. Declare database schema

Before declaring your schema, think carefully what type of information you plan to store in the database and what are the stored properties by which you are going to look up information. Any indexed properties need to be defined in the schema declaration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Dexie from 'dexie';

export default class Database {

constructor(){

const db = new Dexie('db');

db.version(1).stores({
pets: 'id,name,age',
friends: '++id,name'
// you can add more tables here
});
}
}

You may store information not specified in the schema, but since it will not indexed, you cannot query data by that property. For example, given the above schema definition, I might store a pet object such as this one:

1
2
3
4
5
6
{
"id": 1221,
"name": "Rocky",
"age": 5,
"image": "data:image/jpeg;base64,/9j/4gIcSUNDX1BAAAIMbGNtcwIQAABt..."
}

In essence, your schema should contain the properties you are going to use to lookup information. If you are familiar with relational databases where you must explicitly define each stored property, you will notice this to be a big difference between relational schemas and indexedDB.

As with other databases, you have multiple options for choosing your method for setting primary keys. This topic is way beyond the depth of this article, but if you need a refresher, you may read more about setting primary keys with dexie here.

6. Create basic CRUD methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Dexie from 'dexie';

export default class Database {

constructor(){

this.db = new Dexie('db');

this.db.version(1).stores({
pets: 'id,name,age'
});
}

// get single pet by id
getPet = (id) => {
return this.db.pets.get(id);
};

// get list of pets sorted by age
queryPets = () => {
return this.db.pets.orderBy('age').toArray();
};

// create or update pet
savePet = (obj) => {
return this.db.pets.put(obj);
};

// delete pet
deletePet = (id) => {
return this.db.pets.delete(id)
};
}

Dexie includes many operations for working with the stored data. The examples above are simply basic operations to get you started. For more in-depth examples, study the full documentation here.

7. Use database in your application.

You now have a fully implemented database interface for storing user’s data on local machine. The following is an example of how to use the database in your app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Database from './db'

class App{

constructor(){
const db = new Database();

// save some sample object
db.savePet({
"id": 1221,
"name": "Rocky",
"age": 5,
"image": "data:image/jpeg;base64,/9j/4gIcSUNDX1BAAAIMbGNtcwIQAABt..."
}).then(() => {
// read that sample object
db.queryPets().then(pets => {
console.log(pets);
})
});
}
}

Happy Coding!

If you enjoyed this article please share it with your friends!