Using JavaScript's IndexedDB in your Front-End Web Applications

by Michael Szul on

No ads, no tracking, and no data collection. Enjoy this article? Buy us a ☕.

Browsers have a long history of lacking solid storage options, but with the mobile-first web trend, and people moving towards faster, disconnected applications, the need has arisen over the last 5-10 years for solid storage that doesn't include cookies. JavaScript has access to a couple of these storage mechanisms, the three most important being localStorage, sessionStorage, and IndexedDB.

For simple web applications, localStorage and sessionStorage work just fine. The latter represents storage that persists until a user's session ends, while the former represents storage that can persist for an extended period of time. Both are "blocking," which means that neither is asynchronous--stopping the application from operating until objects are saved/retrieved. To get around this blocking, you can use IndexedDB. Keep a bottle of aspirin handy.

I recently incorporated IndexedDB and localStorage into the metron library in order to avoid redundant AJAX calls on subsequent pages for global configuration items. In order to follow best practices, I started with IndexedDB and used localStorage as a fallback option.

The first problem you'll run into is that not all browser support IndexedDB, and those that do (most modern browsers), will do so with a prefix, since the functionality is new. You'll have to set your application up to account for this. Here's some TypeScript code that does that:

(<any>window).indexedDB = (<any>window).indexedDB || (<any>window).mozIndexedDB || (<any>window).webkitIndexedDB || (<any>window).msIndexedDB;
      (<any>window).IDBTransaction = (<any>window).IDBTransaction || (<any>window).webkitIDBTransaction || (<any>window).msIDBTransaction || { READ_WRITE: "readwrite" }; 
      (<any>window).IDBKeyRange = (<any>window).IDBKeyRange || (<any>window).webkitIDBKeyRange || (<any>window).msIDBKeyRange;
      

The next thing to do is open the database, and create the object storage if one doesn't exist:

var db;
      let request = window.indexedDB.open("DB_NAME", 1);
      request.onerror = function(evt) {
          console.log("Warning: Access to IndexedDB for application has been rejected.");
      };
      request.onupgradeneeded = function (evt) {
          let objectStore = (<any>evt.currentTarget).result.createObjectStore("DB_STORE_NAME", { keyPath: "name" });
          objectStore.createIndex("name", "name", { unique: true });
          objectStore.transaction.oncomplete = function(oevt) {
              console.log(`Info: Object store has been successfully created. ${objectStore}`);
          };
      };
      request.onsuccess = function(evt) {
          db = (<any>evt.target).result;
          console.log(`Info: Database initialized. ${db}`);
      };
      

A couple of important things are happening here. The first is that we're opening the database, and giving it a version number. This request object for opening the database has several events tied to it. If access to the database is reject, the error event is fired. If not, and this is the first time to database and object store is being created, the onupgradeneeded event is fired. Once that completes, the success event is called, which will deliver you the database as the result of the target event.

The onupgradeneeded event is important. If you fundamentally modify a database object store structure, you'll have to increment the database version, which will in turn upgrade the object store. When you create the object store, you provide it a key path for access. In addition, you can specify various indices to help with access and searching. These indices can be have constraints on them similar to relational database constraints.

In order to read or write to the database object store, you have to grab an instance of it. You can do this by creating a convenience method (this assumes db is global):

private getObjectStore(): any {
          try {
              let transaction = db.transaction("DB_NAME", "readwrite");
              return transaction.objectStore("DB_STORE_NAME");
          }
          catch(e) {
              console.log(`Error: Failed to get object store. ${e}`);
              return null;
          }
      }
      

Once you have the object store, it's pretty easy to retrieve, save, and delete data:

Get data:

let objectStore = getObjectStore();
      
      var request = objectStore.get("KEY_TO_GET");
      request.onsuccess = function(evt) {
          console.log(evt.target.result);
      };
      

Save data (note, that you can use objectStore.post() or objectStore.put() to insert/update data. put() will overwrite existing data in the store if it finds something with the same key):

let item = { "name": "Michael Szul", "description": "Glorious" };
      var request = objectStore.put(item);
      request.onsuccess = function(evt) {
          console.log(`Info: Item added to the object store. ${evt.target.result}`);
      };
      

Delete data:

var request = getObjectStore().delete("KEY_TO_DELETE");
      request.onsuccess = function(evt) {
          console.log("Info: Object deleted.");
      };
      

IndexedDB is non-blocking, which means that if your code needs to wait on the operation, you'll have to put it in the success event of the transaction call, or you can rely on Promises, and then execute your own code in the then() method. Once you get a hang of the mental jump ropes, it'll all start to come together.