Improve your cache using the hated IndexedDB

Image that you want to manage a Twitter-like timeline using a socket connection in order to receive the posts created one at a time. You want to display the received posts but you want also stored it and remove the oldest post archived. A database is the best solution for this and, in fact, the modern browsers provide us a database, called IndexedDB, and even if it has a bad reputation we will try to understand better what it is and how to use it to improve the cache of our applications

What is IndexedDB?

IndexedDB is a JavaScript-based object-oriented database.

But, to enter the technicalities, IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. His API uses indexes to enable high-performance searches of this data. While Web Storage is useful for storing smaller amounts of data, it is less useful for storing larger amounts of structured data. IndexedDB lets you store and retrieve objects that are indexed with a key, any objects supported by the structured clone algorithm can be stored.

With IndexedDB you can have multiple databases with whatever name you give them (generally you’ll only have one database per app). Every database can contain multiple objects stores, like notifications, posts etc. Every objects store can contain multiple values that can be Javascript objects, strings, arrays, dates, etc… Every item inside the object’s store can have a primary key.

All read or write operations in an IndexedDB must be part of a transaction, this means that if you create a transaction for a series of steps and on of this fail, none of them are applied.

The browser support is good as well with every major browser supporting it.

Why IndexedDB have a bad reputation?

The bad reputation comes from the fact that the API is a little bit horrid, for this advice I recommend the use of IndexedDB Promised a tiny library that mirrors IndexedDB, but replaces the weird IDBRequest objects with promises.


Base IndexedDB interactions

Using indexedDB is really very simple, especially with the library that I have recommended, let’s see together how to create and exploit a database connection.

Open a connection

The open connection want 3 parameters

  • The DB Name
  • The DB Version
  • A callback that returns the upgradeDB parameter. This is the only place where you can create and remove objects stores and indexes.
import idb from 'idb'
idb.open('test-db', 1, upgradeDB => {
  //To create a simple key value object store
var keyValStore = upgradeDB.createObjectStore('keyval')

//To create a store with objects
var objectStore = upgradeDB.createObjectStore('people',{
keyPath: 'name'
})
})

Read and Write on an Object Store

To read and write on an Object Store we need to create a transaction, to do this we just need to use the db.transaction API provided us, get the object store and use the put or get

const dbPromise = idb.open('test-db', 1, upgradeDB => {
var keyValStore = upgradeDB.createObjectStore('keyval')
keyValStore.put("hello", "word")
var objectStore = upgradeDB.createObjectStore('people',{
keyPath: 'name'
})
})
dbPromise.then(db=>{
const tx = db.transaction('keyval')
const keyValStore = tx.objectStore('keyval')
return keyValStore.get('hello')
}).then(val => console.log('the value of hello is', val))
dbPromise.then(db=>{
const tx = db.transaction('keyval', 'readwrite')
const keyValStore = tx.objectStore('keyval')
keyValStore.put('bar', 'foo')
return tx.complete
}).then(() => console.log('added foo:bar'))
dbPromise.then(db=>{
const tx = db.transaction('people', 'readwrite')
const peopleStore = tx.objectStore('people')
return peopleStore.put({
name: 'Sam'
age: 32
})

})

Use the indexes

Indexes are the best mode to group or reorder (make a query) on your objectStore, to do this we just need to upgrade our version of the db and create an index on the objectStore with the .createIndex API. In the example above explains how to order the people by the age and how to receive only the people that have 21 yo.

const dbPromise = idb.open('test-db', 2, upgradeDB => {
switch(upgradeDB.oldVersion){
case 0:
var keyValStore = upgradeDB.createObjectStore('keyval')
keyValStore.put("hello", "word")
var objectStore = upgradeDB.createObjectStore('people',{
keyPath: 'name'
})
case 1:
var peopleStore = upgradeDB.transaction.objectStore('people')
peopleStore.createIndex('age','age')
}
})
dbPromise.then(db=>{
const tx = db.transaction('people')
const peopleStore = tx.objectStore('people')
const ageIndex = peopleStore.index('age')
return ageIndex.getAll()

}).then(people => console.log('People',people))
dbPromise.then(db=>{
const tx = db.transaction('people')
const peopleStore = tx.objectStore('people')
const ageIndex = peopleStore.index('age')
return ageIndex.getAll('21')
}).then(people => console.log('People',people))

Use the cursors

But how we can delete it or just update an element, or more, of our database? To do this the IndexedDB give us the power of the cursor. The library we are using makes available to us the .openCursor() API to iterate on the objects. Inside the loop we will able to check the elements and update or delete it (for example delete all elemenst that are oldest than 18yo)

dbPromise.then(db=>{
const tx = db.transaction('people')
const peopleStore = tx.objectStore('people')
const ageIndex = peopleStore.index('age')
return ageIndex.openCursor()
}).then(function logPerson(cursor){
if(!cursor) return;
console.log('Cursor at: ', cursor.value.name')
//cursor.update(newValue)
//cursor.delete()

return cursor.continue().then(logPerson)
}).then(()=> console.log('Done cursoring')

We can also skip some items just using the .advance API

dbPromise.then(db=>{
const tx = db.transaction('people')
const peopleStore = tx.objectStore('people')
const ageIndex = peopleStore.index('age')
return ageIndex.openCursor()
}).then(cursor => {
if(!cursor) return;
return cursor.advance(2)
})
.then(function logPerson(cursor){
if(!cursor) return;
console.log('Cursor at: ', cursor.value.name')
//cursor.update(newValue)
//cursor.delete()
return cursor.continue().then(logPerson)
}).then(()=> console.log('Done cursoring')

Ok, really nice but how can I use it for the cache?

The title was talking about cache and of course you’re wondering: “ok, but how can you help me with the cache?” Let’s start by saying that there are various types of cache systems that can be implemented. Every cache system depends on your data and what your goals are. the one I would like to focus on are: performance, user experience and the offline first.

Performance and user experience

Be able to store data just received before displaying it allows you to speed up the rendering time of your page and not have to ruin the user experience of your application in case of low connection. Imagine having the data saved in the IndexedDB displayed immediately and then make a request to update the data. The connection point between your interface and the data displayed will be the db. You do not care if the server is down for some reason or if the user has no internet, your interface will be populated with data and in case you can notify the user that the data displayed are not updated for connection problems.

Offline first

Once you have decided on the type of cache to use and have implemented all the steps, be able to make your application offline first is really easy. The extension of your application and the use of a service worker will allow you to show the most important data to your user even if it is offline. I do not want to dwell on the importance of the offline first but knowing that you have structured your application to give a better experience to your users without a so much effort is really important.

We have finished, remember only one last thing, the local storage is born for very small amounts of data, so if your question is “but I can not do it with local storage?” the answer is no, please, no.


If you liked the article please clap and follow me :) 
Thx and stay tuned 🚀
Follow my last news and tips on Facebook