DEV Community

Cover image for A complete guide to using IndexedDB
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

A complete guide to using IndexedDB

Written by Chibuike Nwachukwu✏️

Data storage is an important part of most web applications, from tracking user data to application data. With the rapid development of faster and more robust web applications, efficient client storage is needed to aid development.

Client-side storage on the web has evolved a lot over the years, from cookies used to store user data to the advent of WebSQL (currently deprecated), which allowed developers to store data in a SQL database in the browser, and in turn allowing users familiar with SQL to build robust applications easily.

IndexedDB is an alternative to WebSQL and provides more storage capacity than its previous counterpart. In this tutorial, we’ll explore how to use and set up IndexedDB for web application data storage and how to manipulate its data using the available API.

You can find a link to the public GitHub repo that contains the project we created in this article. We’ll cover the following:

Let’s get started!

What is IndexedDB?

IndexedDB is a low-level API for client-side storage. It is a full-blown, persistent NoSQL storage system available in the browser that allows for the storage of different types of data, such as:

  • Files or blobs
  • Images and videos
  • Structured data like objects, lists, and arrays

IndexedDB can be used in various scenarios, such as caching, PWAs, and gaming, and also supports transactions. It is developed to suit the multiple needs of web apps effectively.

Setting up our project

We won't be doing any fancy setup, as IndexedDB runs natively on the web. First, create a new directory to house the project:

 mkdir indexed-db && cd indexed-db
Enter fullscreen mode Exit fullscreen mode

Now, we’ll create an index.html file to view our application and an index.js script file to store our application logic:

touch index.html index.js styles.css
Enter fullscreen mode Exit fullscreen mode

Saving data to the IndexedDB database

To see the benefits of using this database and learn how to interact with the API, we’ll create a basic to-do application. We enable an Add feature to see how to save data to the database, another to view all to-dos, and a Remove feature to see the GET and DELETE functions of the API, respectively.

Open the index.html created in the previous section and add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODO APP</title>
    <script src="index.js" defer></script>
    <link href="style.css" rel="stylesheet">
</head>
<body>
    <h1>TODO APP</h1>
    <section>
        <aside class="view">
            <h2>TODOs</h2>
            <div class="todos">
                <ol></ol>
            </div>
        </aside>
        <aside class="add"> 
            <h2>Add Todo</h2>
            <form>
              <div>
                <label for="title">Todo title</label>
                <input id="title" type="text" required>
              </div>
              <div>
                <label for="desc">Todo description</label>
                <input id="desc" type="text" required>
              </div>
              <div>
                <button>Save</button>
              </div>
            </form>
        </aside>
    </section>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This creates the base structure of our web app. We do two main things here: first, we create a section to display/view all to-dos saved in the database, and second, we create a section for adding to-dos to the database.

Let’s also add some basic styling to the application. Open the styles.css file and add the following:

  html {
    font-family: sans-serif;
  }

  body {
    margin: 0 auto;
    max-width: 800px;
  }

  header, footer {
    background-color: blue;
    color: white;
    padding: 0 20px;
  }

  .add, .view {
    padding: 30px;
    width: 40%;
  }

  .add {
    background: #ebe6e6; 
  }
  section {
    padding: 10px;
    background: #3182d4;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
  }

  h1 {
    margin: 0;
  }

  ol {
    list-style-type: none;
  }

  div {
    margin-bottom: 10px;
  }
Enter fullscreen mode Exit fullscreen mode

The index.js file is the heart of the application, as it contains the logic for interacting between the app and IndexedDB.

First, we need to create the database; then, we can initialize it by creating an object store (similar to a table in SQL), which we‘ll use to store each item detail. Open the index.js file and add the following logic to it:

let db;
const openOrCreateDB = window.indexedDB.open('todo_db', 1);

openOrCreateDB.addEventListener('error', () => console.error('Error opening DB'));

openOrCreateDB.addEventListener('success', () => {
  console.log('Successfully opened DB');
  db = openOrCreateDB.result;
});

openOrCreateDB.addEventListener('upgradeneeded', init => {
  db = init.target.result;

  db.onerror = () => {
    console.error('Error loading database.');
  };

  const table = db.createObjectStore('todo_tb', { keyPath: 'id', autoIncrement:true });

  table.createIndex('title', 'title', { unique: false });
  table.createIndex('desc', 'desc', { unique: false });
});
Enter fullscreen mode Exit fullscreen mode

As can be seen above, a database named todo_db is created, and then an object store named todo_tb is created with two indexes, title and desc. These indexes allow their values to be duplicated in the store, which is similar to creating a table in SQL and then creating two columns.

Next, to add the Save functionality, we proceed to retrieve the values entered into the form and then save them to the database:

const todos = document.querySelector('ol');
const form = document.querySelector('form');
const todoTitle = document.querySelector('#title');
const todoDesc = document.querySelector('#desc');
const submit = document.querySelector('button');

form.addEventListener('submit', addTodo);

function addTodo(e) {
  e.preventDefault();
  const newTodo = { title: todoTitle.value, body: todoDesc.value };
  const transaction = db.transaction(['todo_tb'], 'readwrite');
  const objectStore = transaction.objectStore('todo_tb');
  const query = objectStore.add(newTodo);
  query.addEventListener('success', () => {
    todoTitle.value = '';
    todoDesc.value = '';
  });
  transaction.addEventListener('complete', () => {
    showTodos();
  });
  transaction.addEventListener('error', () => console.log('Transaction error'));
}
Enter fullscreen mode Exit fullscreen mode

After adding the values to the store, the two form fields are emptied so the user can enter a new item to their list. We can update the view by calling the showTodos method, which we’ll see in the next section.

Retrieving and displaying data

To confirm if the Save to-do function worked, open and use your browser’s Inspect function. In Chrome, you can see IndexedDB under the Application tab, in Storage. As can be seen in the image below, we created the database and saved the first to-do to the todo_tb object store: todo_tb shows one item

To display the available to-dos when the user loads the page, and provide a view of previously added and removed to-dos, we’ll create a method called showTodos:

function showTodos() {
  while (todos.firstChild) {
    todos.removeChild(todos.firstChild);
  }
  const objectStore = db.transaction('todo_tb').objectStore('todo_tb');
  objectStore.openCursor().addEventListener('success', e => {

    const pointer = e.target.result;
    if(pointer) {
      const listItem = document.createElement('li');
      const h3 = document.createElement('h3');
      const pg = document.createElement('p');
      listItem.appendChild(h3);
      listItem.appendChild(pg);
      todos.appendChild(listItem);
      h3.textContent = pointer.value.title;
      pg.textContent = pointer.value.body;
      listItem.setAttribute('data-id', pointer.value.id);
      const deleteBtn = document.createElement('button');
      listItem.appendChild(deleteBtn);
      deleteBtn.textContent = 'Remove';
      deleteBtn.addEventListener('click', deleteItem);
      pointer.continue();
    } else {
      if(!todos.firstChild) {
        const listItem = document.createElement('li');
        listItem.textContent = 'No Todo.'
        todos.appendChild(listItem);
      }

      console.log('Todos all shown');
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

This method gets the to-dos from the store, loops through each item, and creates an HTML element for each. It appends the item to the ol list element on the webpage and passes the id of each to-do into a data attribute called data-id. We’ll use this unique ID later, when we cover the deleteItem function, to identify each to-do when we need to remove it from the store.

To fetch the to-dos on page load, modify the openOrCreateDB success event listener to this:

openOrCreateDB.addEventListener('success', () => {
  console.log('Successfully opened DB');
  db = openOrCreateDB.result;
  showTodos();
});
Enter fullscreen mode Exit fullscreen mode

Deleting data from the database

Finally, let’s test the DELETE API for this database and create a Delete function for our to-do list app:

function deleteItem(e) {
  const todoId = Number(e.target.parentNode.getAttribute('data-id'));
  const transaction = db.transaction(['todo_tb'], 'readwrite');
  const objectStore = transaction.objectStore('todo_tb');
  objectStore.delete(todoId);
  transaction.addEventListener('complete', () => {
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
    alert(`Todo with id of ${todoId} deleted`)
    console.log(`Todo:${todoId} deleted.`);
    if(!todos.firstChild) {
      const listItem = document.createElement('li');
      listItem.textContent = 'No Todo.';
      todos.appendChild(listItem);
    }
  });
  transaction.addEventListener('error', () => console.log('Transaction error'));
}
Enter fullscreen mode Exit fullscreen mode

This deletes the particular to-do using the unique ID passed to the method and removes the element from the webpage. Once it deletes the last to-do item in the store, it shows a “no to-dos” message in the place of the to-do list.

To confirm that the to-do has been deleted from the database, proceed to inspect the webpage and click the Application tab. As can be seen, the todo_tb object store now contains no items: todo_tb is empty The final web application looks like this: The final app view

Incrementing the IndexedDB version

IndexedDB also allows developers to increment the database version. When you open the database, specify your desired version number:

window.indexedDB.open('todo_db', 1);
Enter fullscreen mode Exit fullscreen mode

If the database doesn't exist, it will be created with the specified version. If the database already exists, the version number is checked.

If the version number specified during the open method call is higher than the existing version, a version change event is triggered via the onUpgradeNeeded event. This event allows you to perform database schema changes or data migrations.

A point to note here is deleting a previous object store to add new options when creating a new store would also delete all other data in the old store. Take care to read the old content out and save it somewhere else before upgrading the database.

Drawbacks of using IndexedDB

Since IndexedDB relies on the client's web browser, it is typically more suitable for individual users or smaller-scale applications. Though it can handle a significant amount of data, there are certain considerations to keep in mind when using IndexedDB in large-scale apps or apps used by multiple people.

Scalability limitations

IndexedDB operates within a web browser, which means it is limited to the capabilities and resources of the client-side environment. It may not scale well for scenarios that require handling large numbers of simultaneous users or extremely high throughput.

Data size limitations

Different web browsers impose limits on the maximum amount of data that can be stored in IndexedDB. These limits vary across browsers and can range from a few megabytes to several hundred megabytes.

It is greatly important to be aware of these limitations and design the application accordingly. Once your data storage runs out, you won't be able to store new data in the database, as you would trigger a QuotaExceededError error.

Synchronization challenges

IndexedDB doesn’t provide inbuilt mechanisms for handling data synchronization between clients or handling conflicts in a distributed environment. You would need to implement custom synchronization logic to handle these scenarios. Maintaining data consistency and synchronization across different instances of the application becomes complex.

Hence, for larger-scale applications or applications used by multiple people, it is more efficient to use server-side databases or cloud-based storage solutions.

Conclusion

In this article, we learned about IndexedDB, a database on the web, and how to interact with it to store web application data using JavaScript.

Hopefully, you enjoyed this article and have learned a new way of managing your application data locally on the web. Thanks for reading!

Top comments (0)