Mastering IndexedDB: A Developer’s Guide to Efficient Client-Side Storage

Nazrul IslamNazrul Islam
4 min read

Data storage is a crucial part of most web applications, whether for tracking user data or managing application data. As modern web applications become faster and more robust, efficient client-side storage solutions are needed to enhance performance and development.

Introduction to Client-Side Storage

This tutorial explores how to use IndexedDB for web application data storage, including setting it up and manipulating data using its API. By the end of this guide, you'll have a functional to-do application that utilizes IndexedDB.

What is IndexedDB?

IndexedDB is a low-level API for client-side storage. It provides a persistent NoSQL database in the browser, allowing applications to store various types of data, such as:

  • Files and blobs

  • Images and videos

  • Structured data (objects, lists, and arrays)

IndexedDB is widely used for progressive web apps (PWAs), caching, and gaming applications, as it supports transactions and large-scale data storage.

Base HTML Structure

Create a basic to-do application with an interface for viewing and adding tasks. Open index.html and add the following:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODO APP</title>
    <script src="index.js" defer></script>
    <link href="styles.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>

This structure includes:

  1. A section to display to-dos stored in IndexedDB.

  2. A form to add new to-dos to the database.

Basic Styling

Open styles.css and add the following styles:

html {
    font-family: sans-serif;
}

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

h1 {
    text-align: center;
}

section {
    display: flex;
    justify-content: center;
    padding: 10px;
    background: #3182d4;
}

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

.add {
    background: #ebe6e6;
}

ol {
    list-style-type: none;
}

Initializing IndexedDB

Now, let’s create the IndexedDB database and an object store (todo_tb) to store our to-do items. Open index.js and add the following:

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 });
});

Explanation:

  • Creates an IndexedDB database (todo_db).

  • Creates an object store (todo_tb) with an auto-incrementing ID.

  • Adds indexes for title and desc.


Saving Data to IndexedDB

Next, let’s implement the function to save to-do items 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'));
}

Retrieving and Displaying Data

To display stored to-dos, modify the IndexedDB success event listener:

openOrCreateDB.addEventListener('success', () => {
  db = openOrCreateDB.result;
  showTodos();
});

Then, define showTodos to fetch and display stored items:

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');
    }
  });
}

Deleting Data

To implement deletion, define the deleteItem function:

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'));
}

Conclusion

IndexedDB is a powerful storage solution for modern web applications. In this tutorial, we:

  • Set up an IndexedDB database

  • Implemented Add, Retrieve, and Delete functionalities

  • Built a to-do application using IndexedDB

Drawbacks of IndexedDB

  • Complex API compared to LocalStorage

  • Asynchronous operations make debugging harder

  • Browser limitations (some older versions don’t support it fully)

0
Subscribe to my newsletter

Read articles from Nazrul Islam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nazrul Islam
Nazrul Islam

💻 Software Engineer 🧑‍💻 Full Stack Developer 🗺️ Based in India 📧 contact@nazrulislam.dev 📧 nazrul@stacknothing.com