Beginner's Guide to IndexedDB: Illustrated with Easy-to-Follow Block Diagrams
What is IndexedDB?
In simple terms, IndexedDB is a client-side storage technique like LocalStorage
and SessionStorage
. But what sets it apart and makes it far more powerful than the other alternatives is it's:
Ability to manage large amounts of structured data including files/blobs or other binary types
Ability to create indexes for efficient querying of data (and hence the name "Indexed" DB)
Support for transactions, ensuring data integrity and consistency, especially in concurrent operations.
Building blocks of an IndexedDB
Database: Represents the entire IndexedDB database. It contains one or more object stores.
Object Store: Similar to tables/collections in traditional databases. It holds records of structured data.
Indexes: Optional structures within object stores for efficient querying of data records.
Eg;
Say we have an object store for storing user details
{name, city, age}
. If we want to retrieve the list of users by city, we can create an index for thecity
property. This index allows us to fetch records efficiently without searching through the entire dataset.Keys: Unique identifiers for each record in an object store. Similar to the primary key in RDBMS.
Data: The actual structured data stored within the object store.
IndexedDB Operations
The operations performed on an IndexedDB can be divided into two phases
Creation/Initialisation Phase
Updation/Querying Phase
Creation/Initialisation Phase
In this phase, we will be creating the database, followed by the ObjectStore
which would eventually hold our data records.
We start by opening a connection to the database, if the database doesn't exist it will be created.
const openRequest =
indexedDB.open
(<db-name>, <db-version>)
It returns an IDBOpenDBRequest
object which has handlers that can be used to handle different connection scenarios like:
onerror
: Connection could not be established.onblocked
: Connection was blocked due to a conflicting connection to the same database, which typically occurs when the same app is open in multiple tabs.onsuccess
: Connection was established successfully and the database can be accessed atopenRequest.result
onupgradeneeded
: Connection was established, but the database doesn't exist, so it needs to be initialized or is outdated and requires syncing with the latest version.Most importantly this handler is where we create new object stores and indexes for object stores.
Updation/Querying Phase
Now that we have our database with an object store ready, let's start performing some basic CRUD operations.
IndexedDB allows us to create transactions to carry out operations on the Object Store data.
What is a transaction?
A transaction can be considered a wrapper around a group of operations to be executed on the database. It guarantees data integrity by ensuring that either all operations within a transaction succeed or none of them do.
Say, we have an e-commerce site, whenever a user adds an item to the cart we also need to update the item quantity, if either of the operations fails it should roll back to the initial state.
Let's explore the sequence of steps required to perform an operation or a set of operations on an Object Store:
For a better understanding, we will try to implement the e-commerce example we discussed above
We begin by creating a transaction. We must specify the Object Stores that the transaction will interact with. If it requires write access, the mode should be set to
readwrite
. This ensures the stores remain locked for other transactions until the current transaction is finished.const txn = db.transaction(["cart","items"], "readwrite")
We obtain the object stores on which we will be performing operations.
const cartStore = txn.objectStore("cart"); const itemsStore = txn.objectStore("items");
Finally, it's time to execute the set of operations as follows:
Get the item from
itemsStore
using theget
methodIf the item is available, reduce the quantity and update
itemsStore
using theput
methodAdd new cart item to
cartStore
using theadd
method
const itemId = 123;
const getItemRequest = itemStore.get(itemId); // 1st operation
getItemRequest.onsuccess = function(event) {
const item = event.target.result;
if (item.quantity >= 0) {
// Item available, reduce the quantity
item.quantity -= quantity;
// Update item inventory (2nd operation)
const updateItemRequest = itemsStore.put(item);
updateItemRequest.onsuccess = function(event) {
// Create new cart item
let newCartItem = {
itemId: productId,
status: "Pending"
};
// Add to cart object store (3rd operation)
let addToCartRequest = cartStore.add(newCartItem);
addToCartRequest.onsuccess = function(event) {
console.log("Added to cart successfully.");
};
addToCartRequest.onerror = function(event) {
console.error("Error adding to cart:", event.target.error);
};
};
updateItemRequest.onerror = function(event) {
console.error("Error updating item inventory:", event.target.error);
};
} else {
console.error("Item out of stock.");
}
};
getItemRequest.onerror = function(event) {
console.error("Error retrieving item information:", event.target.error);
};
In the above example, we observed several methods like get
, put
, and add
. Now, let's delve into the various methods available depending on the type of operation we intend to execute on an Object Store.
Adding/Updating
Deleting
Querying
If we want to query on top of an existing index we can open an index and execute all the existing methods like get
, getAll
, getKey
and getAllKeys
on top of it.
// querying on an index
const cityIndex = userStore.index("city");
const getRequest = cityIndex.getAll("Bangalore");
getRequest.onsuccess = function() {
if(getRequest.result) {
console.log("Users from Bangalore", getRequest.result);
}
};
Cursors
IndexedDB also provides another tool called cursor which is particularly helpful for querying large datasets efficiently. It allows one to traverse through records one by one without loading the entire dataset into memory, which can be beneficial for handling large amounts of data.
To open a cursor we need to use the openCursor()
method on a store or index and then iterate over the result using advance
or continue
methods.
const request = store.openCursor();
request.onsuccess = function() {
const cursor = request.result;
while (cursor) {
console.log(cursor.value);
cursor.continue();
}
};
We will delve deeper into cursors in an upcoming article if needed, please let me know in the comments.
Conclusion
Glad that you've made it this far, kudos to your patience! ๐ I hope I've been able to provide some value.
So to sum it up, we saw what IndexedDB is and what sets it apart from other client-side storage techniques, its building blocks like transactions, object stores, and indexes, and phases involved in creating and updating/querying the database and different methods available to perform CRUD operations on the database.
Lastly, we also saw a brief about cursors and how it is beneficial in handling large datasets.
Ending note: I highly recommend going through the references mentioned below to solidify your understanding and to get an in-depth knowledge about IndexedDB.
References
Subscribe to my newsletter
Read articles from Kanishka Chowdhury directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kanishka Chowdhury
Kanishka Chowdhury
UI Engineer currently simplifying the process of building generative AI bots @yellow.ai. On a journey to make the web a better place by building engaging and performant UIs.