Docs

18.3-IndexedDB

18.3 IndexedDB

Overview

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. It provides a transactional database system with indexes, making it suitable for offline-capable web applications.

Learning Objectives

  • •Understand IndexedDB concepts (databases, object stores, indexes)
  • •Perform CRUD operations with transactions
  • •Work with indexes for efficient querying
  • •Handle versioning and database upgrades
  • •Implement offline data storage patterns

Core Concepts

Database Structure

Database (IDBDatabase)
└── Object Store (like a table)
    ā”œā”€ā”€ Records (key-value pairs)
    └── Indexes (for querying by non-key properties)

Opening a Database

function openDatabase(name, version = 1) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(name, version);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

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

      // Create object stores during upgrade
      if (!db.objectStoreNames.contains('users')) {
        const store = db.createObjectStore('users', {
          keyPath: 'id',
          autoIncrement: true,
        });

        // Create indexes
        store.createIndex('email', 'email', { unique: true });
        store.createIndex('age', 'age');
      }
    };
  });
}

// Usage
const db = await openDatabase('myApp', 1);

CRUD Operations

Create (Add)

function addUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.add(user);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Usage
const id = await addUser(db, {
  name: 'John',
  email: 'john@example.com',
  age: 30,
});

Read (Get)

// Get by key
function getUser(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.get(id);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Get all
function getAllUsers(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.getAll();

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Update (Put)

function updateUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.put(user); // put updates or inserts

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Usage
await updateUser(db, {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
  age: 31,
});

Delete

function deleteUser(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.delete(id);

    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

// Clear all
function clearUsers(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.clear();

    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

Using Indexes

// Query by index
function getUserByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('email');
    const request = index.get(email);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Get all by index value
function getUsersByAge(db, age) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');
    const request = index.getAll(age);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Cursors and Ranges

// Iterate with cursor
function iterateUsers(db, callback) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.openCursor();

    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        callback(cursor.value);
        cursor.continue();
      } else {
        resolve();
      }
    };

    request.onerror = () => reject(request.error);
  });
}

// Query with range
function getUsersInAgeRange(db, minAge, maxAge) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');
    const range = IDBKeyRange.bound(minAge, maxAge);
    const request = index.getAll(range);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// IDBKeyRange methods:
IDBKeyRange.only(value); // Exactly value
IDBKeyRange.lowerBound(value); // >= value
IDBKeyRange.upperBound(value); // <= value
IDBKeyRange.bound(lower, upper); // Between lower and upper

Modern Wrapper Class

class IndexedDBStore {
  constructor(dbName, storeName, version = 1) {
    this.dbName = dbName;
    this.storeName = storeName;
    this.version = version;
    this.db = null;
  }

  async open(upgradeCallback) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this);
      };

      request.onupgradeneeded = (event) => {
        if (upgradeCallback) {
          upgradeCallback(event.target.result, event);
        }
      };
    });
  }

  transaction(mode = 'readonly') {
    return this.db
      .transaction([this.storeName], mode)
      .objectStore(this.storeName);
  }

  async add(data) {
    return this.promisify(this.transaction('readwrite').add(data));
  }

  async put(data) {
    return this.promisify(this.transaction('readwrite').put(data));
  }

  async get(key) {
    return this.promisify(this.transaction().get(key));
  }

  async getAll() {
    return this.promisify(this.transaction().getAll());
  }

  async delete(key) {
    return this.promisify(this.transaction('readwrite').delete(key));
  }

  async clear() {
    return this.promisify(this.transaction('readwrite').clear());
  }

  async count() {
    return this.promisify(this.transaction().count());
  }

  async getByIndex(indexName, value) {
    const store = this.transaction();
    return this.promisify(store.index(indexName).get(value));
  }

  async getAllByIndex(indexName, value) {
    const store = this.transaction();
    return this.promisify(store.index(indexName).getAll(value));
  }

  promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  close() {
    if (this.db) {
      this.db.close();
    }
  }
}

// Usage
const store = new IndexedDBStore('myApp', 'users', 1);
await store.open((db) => {
  if (!db.objectStoreNames.contains('users')) {
    const objectStore = db.createObjectStore('users', {
      keyPath: 'id',
      autoIncrement: true,
    });
    objectStore.createIndex('email', 'email', { unique: true });
  }
});

await store.add({ name: 'John', email: 'john@example.com' });
const users = await store.getAll();

Database Versioning

const request = indexedDB.open('myApp', 2); // Increment version

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

  // Migration from version 1 to 2
  if (oldVersion < 2) {
    // Add new object store
    if (!db.objectStoreNames.contains('posts')) {
      db.createObjectStore('posts', { keyPath: 'id' });
    }

    // Modify existing store (must recreate)
    if (db.objectStoreNames.contains('users')) {
      const store = event.target.transaction.objectStore('users');
      if (!store.indexNames.contains('name')) {
        store.createIndex('name', 'name');
      }
    }
  }
};

Error Handling

async function safeTransaction(db, storeName, mode, operation) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], mode);
    const store = transaction.objectStore(storeName);

    transaction.oncomplete = () => resolve();
    transaction.onerror = () => reject(transaction.error);
    transaction.onabort = () => reject(new Error('Transaction aborted'));

    try {
      operation(store);
    } catch (error) {
      transaction.abort();
      reject(error);
    }
  });
}

Best Practices

  1. •Always handle errors - Both request and transaction errors
  2. •Use indexes - For efficient queries on non-key properties
  3. •Batch operations - Use single transaction for multiple operations
  4. •Version carefully - Plan schema migrations
  5. •Close connections - When done with the database

Summary

ConceptDescription
Object StoreLike a table, holds records
Key PathProperty used as primary key
IndexSecondary key for querying
TransactionGroups operations
CursorIterate over records
Key RangeQuery bounds

Resources

.3 IndexedDB - JavaScript Tutorial | DeepML