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
- ā¢Always handle errors - Both request and transaction errors
- ā¢Use indexes - For efficient queries on non-key properties
- ā¢Batch operations - Use single transaction for multiple operations
- ā¢Version carefully - Plan schema migrations
- ā¢Close connections - When done with the database
Summary
| Concept | Description |
|---|---|
| Object Store | Like a table, holds records |
| Key Path | Property used as primary key |
| Index | Secondary key for querying |
| Transaction | Groups operations |
| Cursor | Iterate over records |
| Key Range | Query bounds |