October 8, 2018











IndexedDB is another API meant for client-side storage. It is good for storing a significant amount of data, including files. IndexedDB is more suitable than WebStorage for keeping structured data, and definitely more adequate for that than cookies. In this article, we go through the main concepts of IndexedDB.

Basic concepts of IndexedDB

IndexedDB is object-oriented and requires you to create an object store for data and put objects inside. Objects stored and retrieved are indexed by the IndexedDB with a key. All of the objects belong in the object stores that act as wrappers for them.

It is a transactional database system, any operation meant to read or write to the database is enclosed in a transaction, which is meant to ensure database integrity. The API is asynchronous and uses events. The IndexedDB is very fast, especially when reading data.

All major browsers support IndexedDB, but you might first want to check if it is available to avoid errors.

If you would like to read about other ways of storing data in the browser, check out my articles about cookies and the Web Storage API

Working with IndexedDB

The database is available in the global window object through the indexedDB property. The first thing to do is to call the open function that will open a connection to the database. This operation performs asynchronously. It returns a request object that we can use to listen to upcoming events.

1 const openRequest = window . indexedDB . open ( 'toDoList' ) ;

upgradeneeded event

The upgradeneeded event fires when a database of a bigger version number than the existing one loads. It happens when you load your database for the first time. The upgradeneeded callback is the place to create new object stores, so if you want to add some new ones to an already existing database, you need to create a new version. You can do it by passing an additional argument to the open function

If you call the window.indexedDB.open method with the same version as before, the upgradeneeded event will not fire!

The object store is a storage mechanism for storing data in a database. The createObjectStore function creates a new object store. It takes two arguments: the name of the store and options.

This is the time to mention a very important topic: keys. Every record in the database needs to have a unique key. There are two types of them:

out-of-line key

This is a key stored separately from the value. You can either generate it automatically by passing autoIncrement: true to the createObjectStore function, or define it every time you add an item to the database, which we will cover in a second.

1 2 3 4 5 6 7 8 const openRequest = window . indexedDB . open ( 'toDoList' ) ; openRequest . addEventListener ( 'upgradeneeded' , ( event ) = > { const database = event . target . result ; database . createObjectStore ( 'TaskStore' , { autoIncrement : true } ) } ) ;

in-line key

That’s a key stored as a part of the value. You define it by passing the keyPath property to the createObjectStore function.

1 2 3 4 5 6 7 8 const openRequest = window . indexedDB . open ( 'toDoList' ) ; openRequest . addEventListener ( 'upgradeneeded' , ( event ) = > { const database = event . target . result ; database . createObjectStore ( 'TaskStore' , { keyPath : 'title' } ) } ) ;

Keep in mind that every key has to be unique. With the code above you will not be able to add two tasks with the same title, even for different people assigned. You can fix that by passing an array to the keyPath so that the key will be a combination of more than one property.

1 2 3 4 5 6 7 8 const openRequest = window . indexedDB . open ( 'toDoList' ) ; openRequest . addEventListener ( 'upgradeneeded' , ( event ) = > { const database = event . target . result ; database . createObjectStore ( 'TaskStore' , { keyPath : [ 'title' , 'assignedTo' ] } ) } ) ;

Thanks to that, two tasks will be unique even if they have the same title, but different people assigned.

success event

When the connection establishes and the upgradeneeded callback finishes, the success fires. This means that you are free to create a transaction and send some data!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const openRequest = window . indexedDB . open ( 'toDoList' ) ; openRequest . addEventListener ( 'upgradeneeded' , ( event ) = > { const database = event . target . result ; database . createObjectStore ( 'TaskStore' , { autoIncrement : true } ) } ) ; openRequest . addEventListener ( 'success' , ( event ) = > { const database = event . target . result ; const transaction = database . transaction ( 'TaskStore' , 'readwrite' ) ; const store = transaction . objectStore ( 'TaskStore' ) ; store . add ( { name : 'Wash the dishes' , assignedTo : 'Marcin' } ) ; } ) ;

The database.transaction function returns a transaction object. You can call the objectStore function to get to any store that you created in the upgradeneeded event callback.

An important argument of the database.transaction is the mode. By default, it is readonly. This mode is faster but prevents you from saving any data to the database.

In the example above we passed the value to the add function. Since we use the key generator here through the autoIncrement: true, it is all that we need.

If you choose to use out-of-line keys without the autoIncrement, you need to pass the key as a second argument.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const openRequest = window . indexedDB . open ( 'toDoList' ) ; openRequest . addEventListener ( 'upgradeneeded' , ( event ) = > { const database = event . target . result ; database . createObjectStore ( 'TaskStore' ) } ) ; openRequest . addEventListener ( 'success' , ( event ) = > { const database = event . target . result ; const transaction = database . transaction ( 'TaskStore' , 'readwrite' ) ; const store = transaction . objectStore ( 'TaskStore' ) ; store . add ( { name : 'Wash the dishes' , assignedTo : 'Marcin' } , 1 ) ; } ) ;

It does not need to be a number, it can also be a string.

Put and delete functions

If you use autoIncrement, you do not provide the key directly. You need to obtain it to modify the existing value. Since the store.add function returns a request object, you can easily get to the key of your new database record through it. It might prove to be useful when trying to modify existing records.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 openRequest . addEventListener ( 'success' , ( event ) = > { const database = event . target . result ; const transaction = database . transaction ( 'TaskStore' , 'readwrite' ) ; const store = transaction . objectStore ( 'TaskStore' ) ; const addRequest = store . add ( { name : 'Wash the dishes' , assignedTo : 'Marcin' } ) ; addRequest . addEventListener ( 'success' , ( event ) = > { const key = event . target . result ; store . put ( { name : 'Wash the dishes' , assignedTo : 'Maciek' } , key ) ; } ) } ) ;

The put function works similar to add, but it updates a given record when provided with an already existing key. This is why it is called an update or insert function.

Same goes for the delete function. It will delete a record if provided with a proper key.

Accessing values

There are two basic ways to access values from a store. The first one is the getAll function that retrieves all objects in the store.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 openRequest . addEventListener ( 'success' , ( event ) = > { const database = event . target . result ; const transaction = database . transaction ( 'TaskStore' , 'readwrite' ) ; const store = transaction . objectStore ( 'TaskStore' ) ; const addRequest = store . add ( { name : 'Wash the dishes' , assignedTo : 'Marcin' } ) ; addRequest . addEventListener ( 'success' , ( ) = > { const getRequest = store . getAll ( ) ; getRequest . addEventListener ( 'success' , ( event ) = > { console . log ( 'Tasks' , event . target . result ) } ) ; } ) } ) ;

The other function is the get function that works similar but needs the key as the argument. It retrieves one objects matching the key.

The getAll function can also accept a search query as an argument, but this is a material for a separate article.

Summary

IndexedDB can be a very good solution for storing and accessing a lot of structured data. In this article, we covered all the fundamentals of using it. It included the basic concepts of how it works, how to open the connection, how to deal with events and how to save and access data. There is a good chance that you didn’t like the event-based nature of working with the IndexDB. If that’s the case, you might be interested in a library IndexedDB Promised that is recommended by the developers working in Google.