HTML5 - IndexedDB
Modern Client-Side Storage: IndexedDB
Section titled “Modern Client-Side Storage: IndexedDB”This tutorial explores IndexedDB, the standard API for robust client-side storage of significant amounts of structured data. It allows web applications to store data persistently within the user’s browser.
Note: The older Web SQL Database API, previously covered here, is now deprecated and should not be used in new projects. It was never formally adopted as a web standard and has been removed from or is unsupported by most modern browsers. IndexedDB is the recommended modern alternative.
IndexedDB provides an asynchronous, transactional, object-oriented database. Instead of tables with rows and columns like SQL databases, it uses object stores containing key-value pairs. It is supported by all modern web browsers.
Core Concepts
Section titled “Core Concepts”Understanding these core concepts is key to working with IndexedDB:
- Database: A container for your data. You open a connection to a specific database by name and version.
- Object Store: Similar to tables in SQL databases. Each object store holds records (JavaScript objects) identified by a key.
- Index: Allows efficient lookup of records based on properties other than the primary key. Like indexes in SQL.
- Transaction: A wrapper around operations ensuring data integrity (atomicity). All reads and writes happen within transactions.
- Request: IndexedDB operations are asynchronous. They return request objects with
onsuccessandonerrorevent handlers to manage results or errors. Modern development often uses Promises to handle these asynchronous operations more easily.
Opening a Database Connection
Section titled “Opening a Database Connection”The first step is to request opening a connection to a database using indexedDB.open(). This method takes the database name and an optional version number.
// Request to open the database named 'myAppDB' with version 1const request = indexedDB.open('myAppDB', 1);Opening a database is an asynchronous operation. You need event handlers to know when it succeeds, fails, or needs a schema upgrade:
onerror: Handles errors during the open request.onsuccess: Fires when the connection is successfully established. The database connection object is available inevent.target.result.onupgradeneeded: Fires if the database doesn’t exist yet or if a higher version number is requested than the existing one. This is the only place where you can create or modify the database schema (object stores and indexes).
let db;
const request = indexedDB.open('myNotesAppDB', 1);
request.onerror = (event) => { console.error(`Database error: ${event.target.errorCode}`);};
request.onsuccess = (event) => { db = event.target.result; console.log(`Database opened successfully.`); // You can start using the database connection (db) here};
request.onupgradeneeded = (event) => { db = event.target.result; console.log(`Database upgrade needed or initial setup.`);
// Create an object store called 'notes' // 'id' will be the key, and it will auto-increment const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
// Create an index named 'title_idx' on the 'title' property objectStore.createIndex('title_idx', 'title', { unique: false });
// Create an index named 'created_idx' on the 'created' property objectStore.createIndex('created_idx', 'created', { unique: false });
console.log('Object store and indexes created.');};Using Promises can significantly simplify handling asynchronous operations. Libraries like idb (by Jake Archibald) provide a thin promise-based wrapper around IndexedDB, but here’s how you might wrap the open request manually:
function openNotesDatabase(dbName, version) { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version);
request.onerror = (event) => { reject(`Database error: ${event.target.errorCode}`); };
request.onsuccess = (event) => { resolve(event.target.result); };
request.onupgradeneeded = (event) => { console.log(`Database upgrade needed for version ${version}.`); const db = event.target.result;
if (!db.objectStoreNames.contains('notes')) { const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }); objectStore.createIndex('title_idx', 'title', { unique: false }); objectStore.createIndex('created_idx', 'created', { unique: false }); console.log('Object store \'notes\' created.'); } // Add more upgrade logic for future versions here if needed }; });}
// Usage:openNotesDatabase('myNotesAppDB', 1) .then(dbInstance => { console.log('Database opened successfully via Promise'); db = dbInstance; // Assign to global or module-level variable if needed // Start using the dbInstance }) .catch(error => { console.error(error); });Performing Operations (CRUD)
Section titled “Performing Operations (CRUD)”All data operations (Create, Read, Update, Delete) must happen within a transaction.
-
Start a transaction: Specify the object store(s) involved and the mode (
'readonly'or'readwrite'). -
Get the object store: Access the desired object store from the transaction object.
-
Perform the operation: Use methods like
add(),put(),get(),getAll(),delete(), oropenCursor(). -
Handle results: Use
onsuccessandonerroron the request object returned by the operation, or better yet, use Promises.
Adding Data (Create)
Section titled “Adding Data (Create)”Use the add() method to add a new object to the store. It fails if an object with the same key already exists. Use put() to add or update (overwrite) an object.
function addNote(db, note) { return new Promise((resolve, reject) => { // Start a read-write transaction const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes');
// Add the note object const request = objectStore.add(note);
request.onsuccess = (event) => { // event.target.result contains the key of the added item resolve(event.target.result); };
request.onerror = (event) => { reject(`Error adding note: ${event.target.errorCode}`); };
// Optional: Handle transaction completion/abortion transaction.oncomplete = () => { console.log('Add transaction completed.'); }; transaction.onerror = (event) => { console.error(`Transaction error: ${event.target.errorCode}`); // Note: Request errors often bubble up to transaction errors too }; });}
// Usage (assuming 'db' is the opened database connection):const newNote = { title: 'My First Note', body: 'This is the content of the note.', created: new Date()};
addNote(db, newNote) .then(addedKey => { console.log(`Note added with key: ${addedKey}`); }) .catch(error => { console.error(error); });Reading Data (Read)
Section titled “Reading Data (Read)”Use get(key) to retrieve a single object by its key, or getAll() to retrieve all objects in the store. You can also use indexes and cursors for more complex queries.
function getAllNotes(db) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readonly'); const objectStore = transaction.objectStore('notes');
// Get all notes const request = objectStore.getAll();
request.onsuccess = (event) => { // event.target.result contains an array of all note objects resolve(event.target.result); };
request.onerror = (event) => { reject(`Error getting notes: ${event.target.errorCode}`); }; });}
// Usage:getAllNotes(db) .then(notes => { console.log('Retrieved notes:', notes); // Update UI with the notes const statusDiv = document.getElementById('status'); // Assuming an element with id='status' statusDiv.innerHTML = `<p>Found ${notes.length} notes.</p>`; notes.forEach(note => { statusDiv.innerHTML += `<p>**${note.title}**: ${note.body}</p>`; }); }) .catch(error => { console.error(error); });Updating Data (Update)
Section titled “Updating Data (Update)”Use the put() method. If an object with the specified key exists, it’s updated; otherwise, a new object is added.
function updateNote(db, note) { // note object must include the 'id' (keyPath) return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes'); const request = objectStore.put(note); // Use put for update
request.onsuccess = (event) => { // event.target.result contains the key of the updated/added item resolve(event.target.result); };
request.onerror = (event) => { reject(`Error updating note: ${event.target.errorCode}`); }; });}
// Usage (assuming you have a note with id=1 retrieved earlier):const noteToUpdate = { id: 1, // Must match the existing key title: 'My Updated Note Title', body: 'The content has been updated.', created: new Date() // Or keep the original date};
updateNote(db, noteToUpdate) .then(updatedKey => { console.log(`Note updated with key: ${updatedKey}`); }) .catch(error => { console.error(error); });Deleting Data (Delete)
Section titled “Deleting Data (Delete)”Use the delete(key) method to remove an object by its key.
function deleteNote(db, key) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const objectStore = transaction.objectStore('notes'); const request = objectStore.delete(key);
request.onsuccess = (event) => { // Success indicates the delete operation was queued resolve(`Note with key ${key} delete request successful.`); };
request.onerror = (event) => { reject(`Error deleting note: ${event.target.errorCode}`); };
// It's often better to resolve/reject on transaction completion transaction.oncomplete = () => { console.log(`Delete transaction for key ${key} completed.`); resolve(`Note with key ${key} successfully deleted.`); // Resolve here for certainty }; transaction.onerror = (event) => { reject(`Delete transaction error: ${event.target.errorCode}`); };
});}
// Usage (deleting the note with id=2):deleteNote(db, 2) .then(message => { console.log(message); }) .catch(error => { console.error(error); });Complete Example
Section titled “Complete Example”Here’s a basic HTML page demonstrating opening the database, adding a note, and then reading all notes, using Promises for cleaner async handling.
<!DOCTYPE HTML><html><head> <title>IndexedDB Notes App</title> <meta charset="UTF-8"></head><body>
<h1>My Notes</h1> <div id="status" name="status"> Loading database... </div>
<div id="notesList"> <!-- Notes will be displayed here --> </div>
<script> let db; // Variable to hold the database connection const dbName = 'myNotesAppDB'; const dbVersion = 1;
// --- Promise-based Helper Functions ---
function openNotesDatabase(name, version) { return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onerror = event => reject(`Database error: ${event.target.errorCode}`); request.onsuccess = event => resolve(event.target.result); request.onupgradeneeded = event => { console.log(`Upgrading database to version ${version}...`); const tempDb = event.target.result; if (!tempDb.objectStoreNames.contains('notes')) { const store = tempDb.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }); store.createIndex('title_idx', 'title', { unique: false }); console.log('Object store \'notes\' created.'); } }; }); }
function performTransaction(db, storeName, mode, action) { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], mode); const objectStore = transaction.objectStore(storeName);
transaction.oncomplete = () => resolve(); // Transaction succeeded transaction.onerror = event => reject(`Transaction error: ${event.target.errorCode}`);
try { action(objectStore, resolve, reject); // Pass store and promise controls to the action } catch (err) { reject(err); transaction.abort(); // Abort transaction on action error } }); }
function addNote(db, note) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readwrite'); const store = transaction.objectStore('notes'); const request = store.add(note); request.onsuccess = event => resolve(event.target.result); // Resolve with the new key request.onerror = event => reject(`Add error: ${event.target.error}`); transaction.onerror = event => reject(`Transaction error: ${event.target.error}`); }); }
function getAllNotes(db) { return new Promise((resolve, reject) => { const transaction = db.transaction(['notes'], 'readonly'); const store = transaction.objectStore('notes'); const request = store.getAll(); request.onsuccess = event => resolve(event.target.result); // Resolve with the array of notes request.onerror = event => reject(`GetAll error: ${event.target.error}`); transaction.onerror = event => reject(`Transaction error: ${event.target.error}`); }); }
function displayNotes(notes) { const listDiv = document.getElementById('notesList'); const statusDiv = document.getElementById('status'); listDiv.innerHTML = ''; // Clear previous list if (notes.length === 0) { statusDiv.textContent = 'No notes stored yet.'; return; } statusDiv.textContent = `Displaying ${notes.length} notes:`; const ul = document.createElement('ul'); notes.forEach(note => { const li = document.createElement('li'); li.textContent = `ID ${note.id}: ${note.title} - ${note.body} (Created: ${new Date(note.created).toLocaleString()})`; ul.appendChild(li); }); listDiv.appendChild(ul); }
// --- Main Logic --- async function initializeApp() { try { db = await openNotesDatabase(dbName, dbVersion); document.getElementById('status').textContent = 'Database opened successfully.';
// Example: Add a note if none exist (simple check) const currentNotes = await getAllNotes(db); if (currentNotes.length === 0) { console.log('Adding initial notes...'); const key1 = await addNote(db, { title: 'Welcome', body: 'This is your first note!', created: new Date() }); const key2 = await addNote(db, { title: 'Reminder', body: 'Learn IndexedDB', created: new Date() }); console.log(`Added notes with keys: ${key1}, ${key2}`); }
// Load and display all notes const notes = await getAllNotes(db); displayNotes(notes);
} catch (error) { console.error('Initialization failed:', error); document.getElementById('status').textContent = `Error: ${error}`; } }
// Start the application initializeApp();
</script>
</body></html>Key Considerations & Best Practices
Section titled “Key Considerations & Best Practices”- Asynchronous Operations: Always remember IndexedDB is asynchronous. Use callbacks, Promises (
async/await), or libraries likeidbto manage this. - Error Handling: Implement
onerrorhandlers for requests and transactions. Errors can occur due to various reasons (quota exceeded, constraints violated, etc.). - Transaction Scope: Keep transactions short and specific to the task. Long-running readwrite transactions can lock the database.
- Schema Versioning: Plan your schema upgrades carefully using the
onupgradeneededevent and incrementing the database version. - Browser Compatibility: While widely supported, always check specific features or limits if targeting older browsers. caniuse.com is a good resource.
- Storage Limits: Browsers impose storage limits, which can vary. Handle potential quota errors gracefully.
- Performance: Use indexes for efficient querying on large datasets. Avoid retrieving large amounts of unnecessary data.
Further Resources
Section titled “Further Resources”For more in-depth information, consult the official documentation and related guides:
- MDN Web Docs - IndexedDB API: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- MDN Web Docs - Using IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
- web.dev - IndexedDB: https://web.dev/articles/indexeddb
- Google Developers - IndexedDB Codelab: https://developers.google.com/web/ilt/pwa/working-with-indexeddb