Introduction to IndexedDB

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.

Inside the browser we can also use:

  • Cookies: can host a very small amount of strings
  • Web Storage (or DOM Storage), a term that commonly identifies localStorage and sessionStorage, two key/value stores. sessionStorage, does not retain data, which is cleared when the session ends, while localStorage keeps the data across sessions

Local/session storage have the disadvantage of being capped at a small (and inconsistent) size, with browsers implementation offering from 2MB to 10MB of space per site.

In the past we also had Web SQL, a wrapper around SQLite, but now this is deprecated and unsupported on some modern browsers, it’s never been a recognized standard and so it should not be used, although 83% of users have this technology on their devices according to Can I Use.

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.

Since the advent of Promises in ES2015, and the subsequent move of APIs to using promises, the IndexedDB API seems a bit old school.

While there’s nothing wrong in it, in all the examples that I’ll explain I’ll use the IndexedDB Promised Library by Jake Archibald, which is a tiny layer on top of the IndexedDB API to make it easier to use.

This library is also used on all the examples on the Google Developers website regarding IndexedDB

Create an IndexedDB Database

Include the idb lib using:

yarn add idb

And then include it in your page, either using Webpack or Browserify or any other build system, or simply:

<script src="./node_modules/idb/lib/idb.js"></script>

And we’re ready to go.

Before using the IndexedDB API, always make sure you check for support in the browser, even though it’s widely available, you never know which browser the user is using:

(() => {
  'use strict'

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

  //...IndexedDB code
})()

How to create a database

Using idb.open():

const name = 'mydbname'
const version = 1 //versions start at 1
idb.open(name, version, upgradeDb => {})

The first 2 parameters are self-explanatory. The third param, which is optional, is a callback called only if the version number is higher than the current installed database version. In the callback function body you can upgrade the structure (stores and indexes) of the db.

We use the name upgradeDB for the callback to identify this is the time to update the database if needed.

Create an Object Store

How to create an object store or add a new one

An object store is created or updated in this callback, using the db.createObjectStore('storeName', options) syntax:

const dbPromise = idb.open('mydb', 1, (upgradeDB) => {
  upgradeDB.createObjectStore('store1')
})
.then(db => console.log('success'))

If you installed a previous version, the callback allows you to perform a the migration:

const dbPromise = idb.open('keyval-store', 3, (upgradeDB) => {
  switch (upgradeDB.oldVersion) {
    case 0: // no db created before
      // a store introduced in version 1
      upgradeDB.createObjectStore('store1')
    case 1:
      // a new store in version 2
      upgradeDB.createObjectStore('store2', { keyPath: 'name' })
  }
})
.then(db => console.log('success'))

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. If you don’t specify a key, IndexedDB will create it transparently for us:

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

but you can specify a specific field of object value to auto increment as well:

upgradeDb.createObjectStore('notes', {
  keyPath: 'id',
  autoIncrement: true
})

As a general rule, use auto increment if your values do not contain a unique key already (for example, an email address for users).

Indexes

An index is a way to retrieve data from the object store. It’s defined along with the database creation in the idb.open() callback in this way:

const dbPromise = idb.open('dogsdb', 1, (upgradeDB) => {
  const dogs = upgradeDB.createObjectStore('dogs')
  dogs.createIndex('name', 'name', { unique: false })
})

The unique option determines if the index value should be unique, and no duplicate values are allowed to be added.

You can access an object store already created using the upgradeDb.transaction.objectStore() method:

const dbPromise = idb.open('dogsdb', 1, (upgradeDB) => {
  const dogs = upgradeDB.transaction.objectStore('dogs')
  dogs.createIndex('name', 'name', { unique: false })
})

Check if a store exists

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

if (!upgradeDb.objectStoreNames.contains('store3')) {
  upgradeDb.createObjectStore('store3')
}

Deleting from IndexedDB

Deleting the database, an object store and data

Delete a database

idb.delete('mydb')
.then(() => console.log('done'))

Delete an object store

An object store can only be deleted in the callback when opening a db, and that callback is only called if you specify a version higher than the one currently installed:

const dbPromise = idb.open('dogsdb', 2, (upgradeDB) => {
  upgradeDB.deleteObjectStore('old_store')
})

To delete data in an object store use a transaction

const key = 232

dbPromise.then((db) => {
  const tx = db.transaction('store', 'readwrite')
  const store = tx.objectStore('store')
  store.delete(key)
  return tx.complete
})
.then(() => {
  console.log('Item deleted')
})

Add an item to the database

You can use the put method of the object store, but first we need a reference to it, which we can get from upgradeDB.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:

idb.open('mydb', 1, (upgradeDB) => {
  keyValStore = upgradeDB.createObjectStore('store0')
  keyValStore.put('Hello world!', 'Hello')
})

To add items later down the road, you need to create a 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 idb.open(), and run:

dbPromise.then((db) => {
  const val = 'hey!'
  const key = 'Hello again'

  const tx = db.transaction('store1', 'readwrite')
  tx.objectStore('store1').put(val, key)
  return tx.complete
})
.then(() => {
  console.log('Transaction complete')
})
.catch(() => {
  console.log('Transaction failed')
})

The IndexedDB API offers the add() method as well, but since put() allows us to both add and update, it’s simpler to just use it.

Getting items from a store

Getting a specific item from a store using get()

dbPromise.then(db => db.transaction('objs')
                       .objectStore('objs')
                       .get(123456))
.then(obj => console.log(obj))

Getting all the items using getAll()

dbPromise.then(db => db.transaction('store1')
                       .objectStore('store1')
                       .getAll())
.then(objects => console.log(objects))

Iterating on all the items using a cursor via openCursor()

dbPromise.then((db) => {
  const tx = db.transaction('store', 'readonly')
  const store = tx.objectStore('store')
  return store.openCursor()
})
.then(function logItems(cursor) {
  if (!cursor) { return }
  console.log('cursor is at: ', cursor.key)
  for (const field in cursor.value) {
    console.log(cursor.value[field])
  }
  return cursor.continue().then(logItems)
})
.then(() => {
  console.log('done!')
})

Iterating on a subset of the items using bounds and cursors

const searchItems = (lower, upper) => {
  if (lower === '' && upper === '') { return }

  let range
  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper)
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper)
  } else {
    range = IDBKeyRange.lowerBound(lower)
  }

  dbPromise.then((db) => {
    const tx = db.transaction(['dogs'], 'readonly')
    const store = tx.objectStore('dogs')
    const index = store.index('age')
    return index.openCursor(range)
  })
  .then(function showRange(cursor) {
    if (!cursor) { return }
    console.log('cursor is at:', cursor.key)
    for (const field in cursor.value) {
      console.log(cursor.value[field])
    }
    return cursor.continue().then(showRange)
  })
  .then(() => {
    console.log('done!')
  })
}

searchDogsBetweenAges(3, 10)