SalakCode SalakCode
Browser APIs

IndexedDB

Client-side storage for structured data

Intermediate
browser storage offline performance

Definition

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files and blobs. It uses object stores with indexes to enable high-performance searches, and works well with Web Workers and WebAssembly.

Basic Concepts

Database Structure

IndexedDB Database
├── Object Store: "users"
│   ├── Index: "email" (unique)
│   ├── Index: "age"
│   └── Records: { id, name, email, age }
├── Object Store: "products"
│   ├── Index: "category"
│   └── Records: { id, name, price, category }
└── Object Store: "orders"
    ├── Index: "userId"
    ├── Index: "date"
    └── Records: { id, userId, items, date, total }

Opening a Database

Database Initialization

const DB_NAME = 'MyAppDB';
const DB_VERSION = 1;

const openRequest = indexedDB.open(DB_NAME, DB_VERSION);

openRequest.onerror = (event) => {
  console.error('Failed to open database:', event.target.error);
};

openRequest.onsuccess = (event) => {
  const db = event.target.result;
  console.log('Database opened successfully');
  
  // Store reference for later use
  window.db = db;
};

// This fires when creating or upgrading database
openRequest.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // Create object stores
  if (!db.objectStoreNames.contains('users')) {
    const userStore = db.createObjectStore('users', { 
      keyPath: 'id',
      autoIncrement: true 
    });
    
    // Create indexes
    userStore.createIndex('email', 'email', { unique: true });
    userStore.createIndex('age', 'age', { unique: false });
  }
  
  if (!db.objectStoreNames.contains('products')) {
    const productStore = db.createObjectStore('products', { 
      keyPath: 'id' 
    });
    productStore.createIndex('category', 'category');
  }
};

CRUD Operations

Adding Data

function addUser(user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    
    const request = store.add(user);
    
    request.onsuccess = () => {
      console.log('User added with ID:', request.result);
      resolve(request.result);
    };
    
    request.onerror = () => {
      reject(request.error);
    };
  });
}

// Usage
addUser({ name: 'John', email: 'john@example.com', age: 30 })
  .then(id => console.log('Added user ID:', id))
  .catch(err => console.error('Failed to add user:', err));

Reading Data

function getUser(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 by index
function getUserByEmail(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);
    };
  });
}

Updating Data

function updateUser(user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    
    const request = store.put(user); // Updates if exists, adds if not
    
    request.onsuccess = () => {
      resolve(request.result);
    };
    
    request.onerror = () => {
      reject(request.error);
    };
  });
}

Deleting Data

function deleteUser(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);
    };
  });
}

Queries and Cursors

Getting All Records

function getAllUsers() {
  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);
    };
  });
}

// With query
function getUsersByAgeRange(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);
    };
  });
}

Using Cursors for Large Datasets

async function* userIterator() {
  const transaction = db.transaction(['users'], 'readonly');
  const store = transaction.objectStore('users');
  
  const request = store.openCursor();
  
  while (true) {
    const result = await new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
    
    if (!result) break;
    
    yield result.value;
    result.continue();
  }
}

// Usage
for await (const user of userIterator()) {
  console.log(user);
  // Process without loading all into memory
}

Promisified API

Helper Library Pattern

class IndexedDBWrapper {
  constructor(dbName, version) {
    this.dbName = dbName;
    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.db);
      };
      request.onupgradeneeded = (event) => {
        if (upgradeCallback) {
          upgradeCallback(event.target.result, event.oldVersion, event.newVersion);
        }
      };
    });
  }
  
  async add(storeName, data) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      const request = store.add(data);
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  async get(storeName, id) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readonly');
      const store = transaction.objectStore(storeName);
      const request = store.get(id);
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  // ... other methods
}

// Usage
const db = new IndexedDBWrapper('MyApp', 1);
await db.open((db, oldVersion, newVersion) => {
  if (!db.objectStoreNames.contains('items')) {
    db.createObjectStore('items', { keyPath: 'id' });
  }
});

await db.add('items', { id: 1, name: 'Item 1' });

Best Practices

Error Handling

function handleDBError(error) {
  switch (error.name) {
    case 'QuotaExceededError':
      console.error('Storage quota exceeded');
      // Clear old data or notify user
      break;
    case 'VersionError':
      console.error('Database version mismatch');
      break;
    case 'AbortError':
      console.error('Transaction was aborted');
      break;
    default:
      console.error('Database error:', error);
  }
}

Transaction Management

// Keep transactions short
function updateMultipleUsers(users) {
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  
  // Queue all operations
  users.forEach(user => {
    store.put(user);
  });
  
  // Wait for completion
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve();
    transaction.onerror = () => reject(transaction.error);
  });
}

Storage Limits

// Check available storage
async function checkStorage() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const { usage, quota } = await navigator.storage.estimate();
    const percentUsed = (usage / quota) * 100;
    
    console.log(`Using ${percentUsed.toFixed(2)}% of quota`);
    
    if (percentUsed > 90) {
      // Clean up old data
      await cleanupOldData();
    }
  }
}

Browser Support

// Feature detection
if ('indexedDB' in window) {
  // Use IndexedDB
} else {
  // Fallback to localStorage or cookies
  console.warn('IndexedDB not supported');
}

// For older browsers, use a polyfill
// or wrap with localStorage fallback
Key Takeaway

IndexedDB is the solution for storing significant amounts of structured data client-side. It supports indexes, transactions, and works in Web Workers. Use wrapper libraries or create promisified wrappers for cleaner code. Always handle quota exceeded errors and keep transactions short for optimal performance.

Resources

Related Topics