IndexedDB is one of the storage capabilities introduced into browsers over the years. It’s a key/value store (a noSQL database) considered to be the definitive solution for storing data in browsers.

It’s an asynchronous API, which means that performing costly operations won’t block the UI thread providing a sloppy experience to users. It can store an indefinite amount of data, although once over a certain threshold the user is prompted to give the site higher limits.

It’s supported on all modern browsers.

It supports transactions, versioning and gives good performance. And compared to local storage, we can store much more data into it.

While you can technically create multiple databases per site, you generally create one single database, and inside that database you can create multiple object stores.

A database is private to a domain, so any other site cannot access another website IndexedDB stores.

Each store usually contains a set of things, which can be

  • strings
  • numbers
  • objects
  • arrays
  • dates

For example you might have a store that contains posts, another that contains comments.

A store contains a number of items which have a unique key, which represents the way by which an object can be identified.

You can alter those stores using transactions, by performing add, edit and delete operations, and iterating over the items they contain.

While I don’t use abstraction libraries in any of the other lessons, in here I will use the IndexedDB Promised Library by Jake Archibald, a tiny layer on top of the IndexedDB API to make it easier to use using promises.

Including the idb library

The simplest way is to use unpkg, by adding this to the page header:

<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>

Check if we can use IndexedDB

Before using the IndexedDB API, always make sure you check for support in the browser. Even though IndexedDB is widely available nowadays, you never know which browser the user is using.

Here’s how to check for it, using 'indexedDB' in window:

(() => {
  'use strict'

  if (!('indexedDB' in window)) {
    console.warn('IndexedDB not supported')
    return
  }

  //...we can safely run IndexedDB code
})()

Creating a database and a store

When we create a database, we must initialize a store. If you are familiar with relational databases like MySQL, a store is like a table.

(async () => {
  //...

  const dbName = 'mydbname'
  const storeName = 'store1'
  const version = 1

  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })
})()

You can check if an object store already exists before creating it, by calling the objectStoreNames() method:

const storeName = 'store1'

if (!db.objectStoreNames.contains(storeName)) {
  db.createObjectStore(storeName)
}

Adding data into a store

Adding data when the store is created, initializing it

You use the put method of the object store, but first we need a reference to it, which we can get from db.createObjectStore() when we create it.

When using put, the value is the first argument, the key is the second. This is because if you specify keyPath when creating the object store, you don’t need to enter the key name on every put() request, you can just write the value.

This populates store0 as soon as we create it:

(async () => {
  //...
  const dbName = 'mydbname'
  const storeName = 'store0'
  const version = 1

  const db = await openDB(dbName, version,{
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
      store.put('Hello world!', 'Hello')
    }
  })
})()

Adding data when the store is already created, using transactions

To add items later down the road, you need to create a read/write transaction, that ensures database integrity (if an operation fails, all the operations in the transaction are rolled back and the state goes back to a known state).

For that, use a reference to the dbPromise object we got when calling openDB, and run:

(async () => {
  //...
  const dbName = 'mydbname'
  const storeName = 'store0'
  const version = 1

  const db = await openDB(/* ... */)

  const tx = db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

  const val = 'hey!'
  const key = 'Hello again'
  const value = await store.put(val, key)
  await tx.done
})()

Example

Getting data from a store

Getting one item from a store: get()

const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)

Getting all the items from a store: getAll()

Get all the keys stored

const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()

Get all the values stored

const items = await db.transaction(storeName).objectStore(storeName).getAll()

Deleting data from IndexedDB

Deleting the database, an object store and data

Delete an entire IndexedDB database

const dbName = 'mydbname'
await deleteDB(dbName)

To delete data in an object store

We use a transaction:

(async () => {
  //...

  const dbName = 'mydbname'
  const storeName = 'store1'
  const version = 1

  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })

  const tx = await db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

  const key = 'Hello again'
  await store.delete(key)
  await tx.done
})()

Example

Migrate from previous version of a database

The third (optional) parameter of the openDB() function is an object that can contain an upgrade function called only if the version number is higher than the current installed database version. In that function body you can upgrade the structure (stores and indexes) of the db:

const name = 'mydbname'
const version = 1
openDB(name, version, {
  upgrade(db, oldVersion, newVersion, transaction) {
    console.log(oldVersion)
  }
})

In this callback, you can check from which version the user is updating, and perform some operations accordingly.

You can perform a migration from a previous database version using this syntax

(async () => {
  //...
  const dbName = 'mydbname'
  const storeName = 'store0'
  const version = 1

  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      switch (oldVersion) {
        case 0: // no db created before
          // a store introduced in version 1
          db.createObjectStore('store1')
        case 1:
          // a new store in version 2
          db.createObjectStore('store2', { keyPath: 'name' })
      }
      db.createObjectStore(storeName)
    }
  })
})()

Unique keys

createObjectStore() as you can see in case 1 accepts a second parameter that indicates the index key of the database. This is very useful when you store objects: put() calls don’t need a second parameter, but can just take the value (an object) and the key will be mapped to the object property that has that name.

The index gives you a way to retrieve a value later by that specific key, and it must be unique (every item must have a different key)

A key can be set to auto increment, so you don’t need to keep track of it on the client code:

db.createObjectStore('notes', { autoIncrement: true })

Use auto increment if your values do not contain a unique key already (for example, if you collect email addresses without an associated name).

Demos and videos


This first video builds a demo available at https://glitch.com/edit/#!/flavio-indexeddb-example-1


This second video builds a demo available at https://glitch.com/edit/#!/flavio-indexeddb-example-2