Working with IndexedDB

When searching for indexedDB on google, the top search results don't provide a good introduction to indexedDB. So I thought it might help others who want to persist some data in the browser (and avoid hitting the limit of localStorages 5 Mb size cap) to more easily get started.

So if you're looking to copy-paste some CRUD code to get started with IndexedDB, copy the code from example.

Table of Contents

Background

IndexedDB is a database built into the browser and a much more powerful storage solution than localStorage.

  • Support for JSON data types as well as additional ones like Date.
  • Support for traditional transactions.
  • Supports indexing and key range queries.
  • Supports cursors for iterating large data sets.

Concepts

  • Database: A database is a collection of Object Stores. Similar to a database in a relational database. It's the highest level concept in IndexedDB.
  • Object Store: An Object Store is a collection of Objects. Similar to tables in a relation database.
  • Object: An object belongs to an Object Store. It's similar to rows in a relational database.
  • Index: Allows one to query on properties other than the primary key. Also useful when you want to add a unique constraint on a property.
  • Transaction: A unit of work. Allows you to perform multiple modifications to a database with consistency. During a transaction, the Object Store is locked so no other part can modify the Object Store. Transactions also have the property that if one of the modifications fail, then all other modifications in the transaction will be rolled back to its previous state.
  • keyPath: A unique id for Objects. Same as Primary Key in relational databases.

Gotcha's

  • You use the open method to create and open a database (the method creates a database if the database doesn't exist). The database version passed to the database is used as a version of your database, not the database's actual IndexedDB version.
  • The callback upgrade is used to initialize your database schema, as well as to handle migrations between different versions of your database.
  • IndexedDB does not enforce consistent types for Objects in the same Object Store.

Example

This example uses the async/await wrapper library idb.


import { openDB, deleteDB } from 'idb';

(async function example() {
  const dbName = 'MY_BLOG';
  const dbVersion = 1;

  // Delete a database. Useful in development when
  // you want to initialize your database schema.
  await idb.deleteDB(dbName);

  const db = await idb.openDB(
    dbName, // string, database name
    dbVersion, // integer, YOUR database version (not IndexedDB version)

    {
      // This callback only runs ONE time per database version.
      // Use it to initialize the database schema.
      upgrade(db) {
        const postStoreName = 'posts';

        // This is how we create object stores
        if (!db.objectStoreNames.contains(postStoreName)) {
          const postStore = db.createObjectStore(postStoreName, {
            keyPath: 'id', // The primary key for each object in the object store.

            // Set autoIncrement to true if you want IndexedDB to handle primary
            // key generation for you, otherwise it's up to you to generate unique keys.
            // Also, If we don't specify a keyPath, IndexedDB creates a key and stores
            // it separately from the data.

            // autoIncrement: true,
          });

          // postStore.createIndex('indexName', 'property', options);
          postStore.createIndex('postType', 'postType', {
            unique: false, // Set to true if you want unique values.
          });

          postStore.createIndex('likes', 'likes');
        }
      },
    },
  );

  /********************
   *** CRUD Example ***
   *******************/

  await db.add('posts', {
    title: 'My blog post title',
    postType: 'blog',
    id: 1,
    likes: 0,
  });

  await db.add('posts', {
    title: 'My blog post title',
    postType: 'project',
    id: 5,
    likes: 10,
  });
  await db.get('posts', 1);
  await db.getAll('posts');

  // If there's already a post with id 1, it will be replaced
  await db.put('posts', {
    title: 'My updated blog post title',
    postType: 'blog',
    id: 1,
    likes: 3,
  });

  // If there's already a post with id 1, the method will throw an error
  await db.add('posts', {
    title: 'My updated blog post title',
    postType: 'blog',
    id: 2,
    likes: 8,
  });

  await db.delete('posts', 1);

  /********************
   *** Transactions ***
   *******************/

  // IndexedDB has two types of transactions, 'read' and 'read-write' transaction.
  // read-write locks the object store during the transaction so no other
  // part can manipulate the store during the transaction.

  let tx = db.transaction('posts', 'readwrite');
  let postStore = tx.objectStore('posts');
  await postStore.add({
    title: 'My second blog post title',
    id: 3,
    likes: 12,
  });
  await postStore.add({
    title: 'My second blog post title',
    id: 4,
    likes: 4,
  });
  await tx.done;

  /*************
   *** Index ***
   ************/

  await db.getAllFromIndex('posts', 'postType');

  // Or return posts with the value 'blog' for the key 'postType'.
  await db.getAllFromIndex('posts', 'postType', 'blog');

  // Or filter on the number of likes, there's bound, lowerBound, upperBound conditionals.
  await db.getAllFromIndex('posts', 'likes', IDBKeyRange.bound(1, 9));

  /***************
   *** Cursors ***
   ***************/

  let cursor = await db.transaction('posts', 'readwrite').store.openCursor();

  while (cursor) {
    // console.log(cursor.key, cursor.value);

    cursor.update({ ...cursor.value, author: 'Samir' });
    cursor = await cursor.continue();
  }
})();

Resources

dark/light theme