How to Use IndexedDB – The Ultimate Beginner‘s Guide

As a front-end developer, you have many options when it comes to storing data on the client-side. For simple key-value pairs, you can use localStorage or sessionStorage. For caching network responses, there‘s the Cache API. But when you need a full-fledged NoSQL database in the browser, IndexedDB is the way to go.

IndexedDB is a transactional database system that allows you to store and retrieve structured data on the client-side. It‘s more powerful than other storage mechanisms and is useful for applications that need to persist a large amount of data and work offline.

In this beginner‘s guide, we‘ll dive into the IndexedDB API and learn how to use it to build data-driven web applications. Let‘s get started!

Why Use IndexedDB?

Before we look at how IndexedDB works, let‘s consider why you might want to use it over other client-side storage options:

Large storage capacity – IndexedDB databases can store a much greater amount of data than other mechanisms (e.g. localStorage is limited to 5MB). The browser determines the limit based on factors such as disk space.

Indexed data – As the name implies, IndexedDB stores data in an indexed structure. You can run queries and retrieve records efficiently using indexes, which is not possible with a key-value store.

Asynchronous API – All IndexedDB operations are asynchronous, meaning they won‘t block the main thread. This keeps your application responsive even when dealing with large datasets.

Transactional – IndexedDB uses a transactional model that allows you to group multiple operations into a single unit of work. If one operation fails, the entire transaction is rolled back.

Works offline – Since IndexedDB is a client-side technology, it keeps working even if the user goes offline. You can sync up any changes later when a network connection is re-established.

Object-oriented – IndexedDB uses an object store model, meaning each record is an object that can contain complex data like nested objects, arrays, dates and more.

These features make IndexedDB well-suited for applications like productivity tools, games, content management systems and anything requiring sophisticated client-side caching and persistence.

Key Concepts and Terminology

To understand how to use IndexedDB effectively, there are a few key concepts you need to grasp:

Database – An IndexedDB database is a collection of object stores. You can think of a database like a namespace. Each origin (protocol + hostname + port) can have any number of databases.

Object store – An object store is like a table in a traditional relational database. It‘s where the actual data objects are stored. Each object store has a name and a primary key that uniquely identifies its records.

Index – An index lets you retrieve records from an object store using properties other than the primary key. For example, if you‘re storing people, you could have an index on the "age" property to quickly look up people by age. Indexes can be auto-generated or custom.

Transaction – A transaction is a wrapper around a group of database operations that ensures they all succeed or fail together. Transactions can be read-only or read/write. They are essential for maintaining data integrity.

Cursor – A mechanism for iterating over multiple records in an object store or index. Cursors are useful for processing large datasets in chunks to avoid consuming too much memory.

Version – An IndexedDB database has a version number that determines its schema (the object stores and indexes it contains). When you make changes to the schema, you open the database with an increased version number to trigger a schema upgrade.

It‘s a lot to take in, but these concepts will make more sense once we start looking at code examples. For now, just be aware that IndexedDB has a specific vocabulary that can be confusing if you‘re coming from a different database background.

Opening a Database

To start using IndexedDB, we first need to open a database. Here‘s a basic example:

let db;

const request = indexedDB.open(‘MyDatabase‘, 1);

request.onerror = function(event) {
  console.error(‘Failed to open database‘, event);
};

request.onsuccess = function(event) {  
  db = event.target.result;
  console.log(‘Database opened successfully‘);
};

Breaking this down:

  1. We declare a global variable db to hold a reference to the database object.

  2. The open() method takes the database name and version number. If the database doesn‘t exist yet, it will be created. If the version is greater than the current version, an upgrade will be triggered.

  3. The open() method returns an IDBOpenDBRequest object which has onsuccess and onerror event handlers.

  4. If the database is opened successfully, the onsuccess handler is called and the database object is available via event.target.result. We assign this to the db variable for later use.

  5. If opening the database fails for some reason (e.g. permissions issue), the onerror handler will be called with details about what went wrong.

Creating Object Stores and Indexes

After opening the database, the next step is to create the object stores and indexes that define our database schema. This is done in the upgradeneeded event handler:

const request = indexedDB.open(‘MyDatabase‘, 1);

request.onupgradeneeded = function(event) {
  const db = event.target.result;

  // Create the ‘people‘ object store if it doesn‘t exist
  if (!db.objectStoreNames.contains(‘people‘)) {
    const peopleOS = db.createObjectStore(‘people‘, { keyPath: ‘email‘ });
    peopleOS.createIndex(‘gender‘, ‘gender‘, { unique: false });
    peopleOS.createIndex(‘ssn‘, ‘ssn‘, { unique: true });
  }

  // Create the ‘notes‘ object store if it doesn‘t exist
  if (!db.objectStoreNames.contains(‘notes‘)) {
    const notesOS = db.createObjectStore(‘notes‘, { autoIncrement: true });
    notesOS.createIndex(‘title‘, ‘title‘, { unique: false });    
  }

  console.log(‘Object stores created‘);
};

This code does the following:

  1. The upgradeneeded event is triggered when the requested database version is greater than the current version. This is where we define the database schema.

  2. We get a reference to the database object via event.target.result.

  3. To create a new object store, we call createObjectStore() on the database object. The first argument is the name of the store. The second argument is an options object specifying the key path and/or auto increment behavior.

  4. The key path is the property that uniquely identifies records in the store. Here we‘re using the ‘email‘ property as the key path for the ‘people‘ store. This means each person record must have a unique email address. You can also use auto-incrementing integer keys by setting autoIncrement to true.

  5. After creating an object store, we can optionally create indexes on properties we want to query on. Here we create a non-unique index on ‘gender‘ and a unique index on ‘ssn‘.

  6. We repeat the process for the ‘notes‘ store, using an auto-incrementing key. We also create an index on the ‘title‘ property.

The upgradeneeded handler is only called when the database is first created or when its version number increases. Note that you can‘t create object stores and indexes outside of this handler.

Adding and Retrieving Data

Now that our database schema is defined, we can start adding and retrieving data from the object stores. Here‘s an example of adding a record:

const transaction = db.transaction([‘people‘], ‘readwrite‘);
const store = transaction.objectStore(‘people‘);

const person = {
  email: ‘[email protected]‘, 
  name: ‘Alice‘,
  gender: ‘female‘,
  ssn: ‘123-45-6789‘
};

const request = store.add(person);

request.onsuccess = function(event) {
  console.log(‘Person added to the store‘, event);
};

request.onerror = function(event) {
  console.error(‘Failed to add person‘, event);
};

The steps are:

  1. Open a readwrite transaction on the ‘people‘ object store. Transactions ensure database integrity.

  2. Get a reference to the ‘people‘ store via transaction.objectStore().

  3. Create an object representing the person record we want to add. The object must have properties matching the store‘s key path and indexes.

  4. Call the add() method on the object store, passing in the person object. This returns an IDBRequest object.

  5. Handle the onsuccess and onerror events to know if the add operation succeeded or failed.

To retrieve a record by its primary key, we use the get() method:

const transaction = db.transaction([‘people‘]);
const store = transaction.objectStore(‘people‘);
const request = store.get(‘[email protected]‘);

request.onsuccess = function(event) {
  const person = event.target.result;
  console.log(‘Retrieved person‘, person);
};

This code opens a read-only transaction, gets the ‘people‘ store, and calls get() with the primary key value. The matching record (if any) is available in the onsuccess event handler via event.target.result.

We can also use indexes to retrieve records by other properties:

const transaction = db.transaction([‘people‘]);
const store = transaction.objectStore(‘people‘);
const index = store.index(‘ssn‘);

const request = index.get(‘123-45-6789‘);

request.onsuccess = function(event) {
  const person = event.target.result;
  console.log(‘Retrieved person by SSN‘, person);  
};

Instead of calling get() on the object store, we first get a reference to the ‘ssn‘ index via store.index() and then call get() on the index, passing the SSN value.

Updating and Deleting Data

To modify an existing record in an object store, we use the put() method:

const transaction = db.transaction([‘people‘], ‘readwrite‘);  
const store = transaction.objectStore(‘people‘);

const person = {
  email: ‘[email protected]‘,
  name: ‘Alice Smith‘,
  gender: ‘female‘, 
  ssn: ‘123-45-6789‘
};

const request = store.put(person);

request.onsuccess = function(event) {
  console.log(‘Person updated‘, event);
};

The put() method works similarly to add(), except that it will overwrite an existing record if the primary key already exists in the store.

To delete a record, we use the delete() method:

const transaction = db.transaction([‘people‘], ‘readwrite‘);
const store = transaction.objectStore(‘people‘);
const request = store.delete(‘[email protected]‘);

request.onsuccess = function(event) {
  console.log(‘Person deleted‘, event);
};

We pass the primary key value to delete() and the matching record will be removed from the store.

Using Cursors

When working with larger datasets, it‘s often necessary to process records in batches to avoid consuming too much memory. IndexedDB provides cursors for this purpose:

const transaction = db.transaction([‘notes‘], ‘readonly‘);  
const store = transaction.objectStore(‘notes‘);

store.openCursor().onsuccess = function(event) {
  const cursor = event.target.result;

  if (cursor) {
    const note = cursor.value;
    console.log(‘Processing note:‘, note);
    cursor.continue();
  } else {
    console.log(‘No more notes‘);
  }
};

Here‘s what‘s happening:

  1. We open a read-only transaction on the ‘notes‘ store and get a reference to the store object.

  2. Calling openCursor() on the store returns an IDBRequest representing an asynchronous cursor request.

  3. When the cursor is ready, the onsuccess handler is called. We get the actual cursor object via event.target.result.

  4. The cursor object has a value property containing the current record. We process this record somehow.

  5. To advance to the next record, we call cursor.continue(). This triggers another onsuccess event with the next record.

  6. If there are no more records, event.target.result will be null, and we‘ve reached the end of the store.

Cursors also support advance() to skip multiple records, and delete() and update() to modify records in-place.

Browser Support and Fallbacks

At the time of writing, IndexedDB is supported in all modern browsers, including Chrome, Firefox, Safari, Edge and Internet Explorer 10+. However, it‘s always a good idea to check for support before using it:

if (‘indexedDB‘ in window) {
  // IndexedDB is supported, so go ahead and use it
} else {
  // Fall back to a different storage mechanism or show an error message
}

If IndexedDB is not available, you‘ll need to fall back to a different storage mechanism like localStorage or a server-side database. Alternatively, you can use a library like localForage that provides a simple, unified API for client-side storage, using IndexedDB under the hood with fallbacks to WebSQL and localStorage.

Conclusion

IndexedDB is a powerful tool for storing and retrieving structured data on the client-side. Its asynchronous API, support for transactions and indexes, and ability to store large amounts of data make it well-suited for building offline-capable, data-intensive web applications.

In this guide, we‘ve covered the key concepts behind IndexedDB and walked through examples of how to open databases, create object stores and indexes, perform CRUD operations, and work with cursors. While the API may seem daunting at first, with a bit of practice it becomes much more approachable.

To learn more about IndexedDB, consult the following resources:

Happy coding!

Similar Posts