JavaScript and IndexedDB: Storing Data
Published June 13, 2024 at 6:40 pm
Introduction to JavaScript and IndexedDB
Are you curious about how to store data client-side using JavaScript? IndexedDB is your answer.
It provides a way to persistently store data in a user’s browser, enabling the development of complex applications that work offline. IndexedDB offers a more significant storage capacity compared to localStorage and is suitable for large amounts of structured data.
In this article, you’ll learn how IndexedDB works, why it’s beneficial, and how to use it in your JavaScript projects.
The aim is to get you comfortable with setting up an IndexedDB database, adding data, fetching data, and handling common issues.
TLDR: How to Store Data Using JavaScript and IndexedDB?
Use IndexedDB to store extensive and structured data client-side in a web application.
You can set up an IndexedDB database and store data using the following example:
// Open (or create) the database
let request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
let objectStore = db.createObjectStore("myStore", { keyPath: "id" });
};
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Add data to your object store
let request = objectStore.add({ id: 1, name: "John Doe", age: 25 });
request.onsuccess = function(event) {
console.log("Data has been added to your database.");
};
// Handle errors
request.onerror = function(event) {
console.log("Unable to add data. It already exists in your database.");
};
};
Why Use IndexedDB?
IndexedDB is ideal for web applications that require local storage of a significant amount of structured data.
It is asynchronous, which means it does not block the main thread.
Unlike localStorage, IndexedDB can store complex data types, including binary data.
It is transactional, ensuring that operations either complete successfully or fail as a whole to maintain data integrity.
Setting Up Your IndexedDB Database
To start with IndexedDB, you need to open a connection to the database. If the database doesn’t exist, it will be created.
The structure of your database is defined in the onupgradeneeded event handler.
Here’s a step-by-step guide:
- Open a connection using
indexedDB.open. - Use
onupgradeneededto create object stores the first time you open the database at a specific version. - Handle success and error events to verify the database connection.
// Open (or create) the database
let request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
let objectStore = db.createObjectStore("myStore", { keyPath: "id" });
};
request.onsuccess = function(event) {
let db = event.target.result;
console.log("Database opened successfully.");
};
request.onerror = function(event) {
console.log("Error opening database:", event);
};
Adding Data to Your IndexedDB Database
Once your database is set up, you can add data to it.
You’ll start a new transaction, get object stores, and perform operations such as adding, updating, or deleting data within these stores.
Here is how to add data:
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Add data to your object store
let request = objectStore.add({ id: 1, name: "John Doe", age: 25 });
request.onsuccess = function(event) {
console.log("Data has been added to your database.");
};
// Handle errors
request.onerror = function(event) {
console.log("Unable to add data. It already exists in your database.");
};
};
Fetching Data from IndexedDB
Fetching data from IndexedDB involves starting a transaction and getting the required object from the object store.
Here is an example of how to fetch data:
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readonly");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Get data from the object store
let request = objectStore.get(1);
request.onsuccess = function(event) {
if (request.result) {
console.log("Data: ", request.result);
} else {
console.log("Data not found.");
}
};
// Handle errors
request.onerror = function(event) {
console.log("Unable to retrieve the data.");
};
};
Handling Errors and Common Issues
Common Issues
- Database Versioning: Always increment the version number when making changes to the database structure.
- Browser Compatibility: Ensure your code handles compatibility across different browsers.
- Transaction Lifespan: Transactions are short-lived. Ensure all operations are done within the transaction lifespan.
Frequently Asked Questions
How does IndexedDB compare to localStorage?
IndexedDB can store large amounts of structured data, whereas localStorage has a limited capacity of about 5MB and can only store strings.
Is IndexedDB asynchronous?
Yes, IndexedDB operations are asynchronous and do not block the main thread.
Can I use IndexedDB for offline applications?
Yes, IndexedDB is suitable for offline applications as it stores data locally in the browser.
How do I handle browser compatibility with IndexedDB?
You can use feature detection to check if IndexedDB is supported and provide fallbacks if necessary.
What happens if I try to add data with a duplicate key?
An error will occur. You can handle duplicate key errors by using event handlers.
How can I delete data from IndexedDB?
You can delete data by starting a transaction and using the delete method on the object store.
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Delete data from the object store
let request = objectStore.delete(1);
request.onsuccess = function(event) {
console.log("Data has been deleted from your database.");
};
// Handle errors
request.onerror = function(event) {
console.log("Unable to delete data.");
};
};
How can I update data in IndexedDB?
You can update data by using the put method within a transaction.
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Update data in the object store
let request = objectStore.put({ id: 1, name: "Jane Doe", age: 26 });
request.onsuccess = function(event) {
console.log("Data has been updated in your database.");
};
request.onerror = function(event) {
console.log("Unable to update data.");
};
};
Why IndexedDB Outshines Other Storage Solutions
IndexedDB surpasses other client-side storage solutions in several key areas.
localStorage is useful for storing small amounts of simple data, but it falls short for complex applications.
IndexedDB, on the other hand, is built to handle large-scale, structured data efficiently.
It is designed to support high-performance searches using indexes, something localStorage lacks.
IndexedDB also offers significant advantages in terms of data security and integrity by using transactions.
Advanced Operations with IndexedDB
IndexedDB supports a variety of advanced operations for more complex use cases.
Some of these include updating, deleting, and searching for specific records.
Understanding how to perform these operations will enable you to build more sophisticated applications.
Updating Data in IndexedDB
To update data in IndexedDB, you use the put method within a transaction.
Here’s an example:
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Update data in the object store
let request = objectStore.put({ id: 1, name: "Jane Doe", age: 26 });
request.onsuccess = function(event) {
console.log("Data has been updated in your database.");
};
request.onerror = function(event) {
console.log("Unable to update data.");
};
};
Deleting Data from IndexedDB
To delete data from IndexedDB, start a transaction and use the delete method on the object store.
Here’s an example:
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readwrite");
// Retrieve an object store
let objectStore = transaction.objectStore("myStore");
// Delete data from the object store
let request = objectStore.delete(1);
request.onsuccess = function(event) {
console.log("Data has been deleted from your database.");
};
request.onerror = function(event) {
console.log("Unable to delete data.");
};
};
Searching for Specific Records
IndexedDB supports advanced querying through the use of indexes.
You need to create indexes during the setup phase using the createIndex method.
Here’s how you can set up an index on the “name” field:
// Open (or create) the database
let request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
let objectStore = db.createObjectStore("myStore", { keyPath: "id" });
// Create an index to search customers by name
objectStore.createIndex("name", "name", { unique: false });
};
request.onsuccess = function(event) {
let db = event.target.result;
console.log("Database and index opened/created successfully.");
};
request.onerror = function(event) {
console.log("Error opening database:", event);
};
Once the index is set up, you can use it to search for records:
// Open the database
let request = indexedDB.open("MyDatabase", 1);
request.onsuccess = function(event) {
let db = event.target.result;
// Start a new transaction
let transaction = db.transaction(["myStore"], "readonly");
// Retrieve the object store and the index
let objectStore = transaction.objectStore("myStore");
let index = objectStore.index("name");
// Query by index
let request = index.get("John Doe");
request.onsuccess = function(event) {
if (request.result) {
console.log("Data: ", request.result);
} else {
console.log("Data not found.");
}
};
request.onerror = function(event) {
console.log("Unable to retrieve the data.");
};
};
Transactions in IndexedDB
IndexedDB operations are performed within transactions to ensure data integrity.
Transactions can be read-only or read-write, depending on the type of operations you plan to perform.
Transactions are short-lived and will automatically commit once all operations are completed successfully.
If an operation fails, the transaction is rolled back to maintain consistency.
Handling Versioning in IndexedDB
Whenever you need to alter the database structure, you should increment the database version number.
IndexedDB will trigger the onupgradeneeded event, where you can handle schema changes.
Carefully manage version numbers to ensure backward compatibility with older versions of your application.
Sample Application: To-Do List with IndexedDB
Let’s build a simple to-do list application to see IndexedDB in action.
Setting Up the Database
First, we set up the database structure:
let request = indexedDB.open("ToDoListDB", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
db.createObjectStore("toDoList", { keyPath: "id", autoIncrement: true });
};
request.onsuccess = function(event) {
let db = event.target.result;
console.log("Database set up successfully.");
};
request.onerror = function(event) {
console.log("Database setup failed:", event);
};
Adding Tasks
Use the following code to add tasks to the database:
let request = indexedDB.open("ToDoListDB", 1);
request.onsuccess = function(event) {
let db = event.target.result;
let transaction = db.transaction(["toDoList"], "readwrite");
let objectStore = transaction.objectStore("toDoList");
let request = objectStore.add({ task: "Finish writing blog post", done: false });
request.onsuccess = function(event) {
console.log("Task added successfully.");
};
request.onerror = function(event) {
console.log("Failed to add task:", event);
};
};
Fetching Tasks
You can fetch tasks from the database as follows:
let request = indexedDB.open("ToDoListDB", 1);
request.onsuccess = function(event) {
let db = event.target.result;
let transaction = db.transaction(["toDoList"], "readonly");
let objectStore = transaction.objectStore("toDoList");
let request = objectStore.openCursor();
request.onsuccess = function(event) {
let cursor = event.target.result;
if(cursor) {
console.log("Task:", cursor.value);
cursor.continue();
} else {
console.log("No more tasks.");
}
};
request.onerror = function(event) {
console.log("Failed to fetch tasks:", event);
};
};
Updating Tasks
Use the following code to update tasks in the database:
let request = indexedDB.open("ToDoListDB", 1);
request.onsuccess = function(event) {
let db = event.target.result;
let transaction = db.transaction(["toDoList"], "readwrite");
let objectStore = transaction.objectStore("toDoList");
let request = objectStore.put({ id: 1, task: "Finish writing blog post", done: true });
request.onsuccess = function(event) {
console.log("Task updated successfully.");
};
request.onerror = function(event) {
console.log("Failed to update task:", event);
};
};
Deleting Tasks
To delete tasks from the database, use the following code:
let request = indexedDB.open("ToDoListDB", 1);
request.onsuccess = function(event) {
let db = event.target.result;
let transaction = db.transaction(["toDoList"], "readwrite");
let objectStore = transaction.objectStore("toDoList");
let request = objectStore.delete(1);
request.onsuccess = function(event) {
console.log("Task deleted successfully.");
};
request.onerror = function(event) {
console.log("Failed to delete task:", event);
};
};
Best Practices for Using IndexedDB
When using IndexedDB, follow these best practices to ensure the best performance and usability.
Use Transaction Management
- Always handle transactions carefully to ensure data integrity.
- Clean up transactions properly to avoid memory leaks.
Handle Errors Gracefully
- Always provide error handlers for database operations.
- Give users feedback on what went wrong and how they can correct it.
Optimize Performance
- Use indexes wisely to speed up search operations.
- Keep transactions short to avoid blocking the main thread.
Common Issues and Troubleshooting
Database Version Mismatch
This often occurs when there are changes to the database structure.
Always increment the version number to trigger the onupgradeneeded event.
Quota Exceeded
This occurs when you try to store more data than the browser allows.
Consider using compression techniques or cleaning up old data.
Compatibility Issues
Different browsers may implement IndexedDB slightly differently.
Use feature detection and polyfills to handle compatibility issues.
Advanced IndexedDB Techniques
Using Promises with IndexedDB
While IndexedDB is asynchronous and uses request events, you can wrap these calls in Promises for a cleaner syntax.
Here’s an example of how to open a database using Promises:
function openDB(name, version) {
return new Promise((resolve, reject) => {
let request = indexedDB.open(name, version);
request.onupgradeneeded = function(event) {
let db = event.target.result;
db.createObjectStore("myStore", { keyPath: "id" });
};
request.onsuccess = function(event) {
resolve(event.target.result);
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
openDB("MyDatabase", 1).then(db => {
console.log("Database opened with Promise.");
}).catch(error => {
console.log("Database failed to open:", error);
});
Applying Composite Keys
Composite keys can be used to create more sophisticated data models.
They allow you to combine multiple fields into a single index key.
Here’s an example of setting up an object store with a composite key:
// Open (or create) the database
let request = indexedDB.open("CompositeKeyDB", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
let objectStore = db.createObjectStore("people", { keyPath: ["lastName", "firstName"] });
};
request.onsuccess = function(event) {
let db = event.target.result;
console.log("Database with composite key opened successfully.");
};
request.onerror = function(event) {
console.log("Error opening database:", event);
};
Composite keys enable more complex querying.
You can query data based on a combination of fields, providing greater flexibility.
Encrypting Data
To enhance security, you may want to encrypt sensitive data before storing it in IndexedDB.
Here’s how you can use the Web Crypto API to encrypt data before storing it:
// Encrypt data
async function encryptData(data, password) {
let enc = new TextEncoder();
let key = await crypto.subtle.importKey(
"raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]);
let derivedKey = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: enc.encode("salt"), iterations: 1000, hash: "SHA-256" },
key, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
let encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: enc.encode("iv") },
derivedKey, enc.encode(data));
return new Uint8Array(encrypted);
}
encryptData("Sensitive Data", "password123").then(encrypted => {
// Store encrypted data in IndexedDB
let request = indexedDB.open("EncryptedDB", 1);
request.onupgradeneeded = function(event) {
let db = event.target.result;
db.createObjectStore("sensitiveStore", { keyPath: "id" });
};
request.onsuccess = function(event) {
let db = event.target.result;
let transaction = db.transaction(["sensitiveStore"], "readwrite");
let objectStore = transaction.objectStore("sensitiveStore");
let addRequest = objectStore.add({ id: 1, data: encrypted });
addRequest.onsuccess = function(event) {
console.log("Encrypted data stored successfully.");
};
addRequest.onerror = function(event) {
console.log("Failed to store encrypted data:", event);
};
};
});
Encrypting data before storage adds an extra layer of security, making your applications more resilient against data breaches.
Commonly Asked Questions
Is IndexedDB suitable for mobile applications?
Yes, IndexedDB is supported on most modern mobile browsers, making it suitable for mobile applications.
How do I clear all data from an IndexedDB database?
You can clear all data by deleting the object stores or using the deleteDatabase method.
let request = indexedDB.deleteDatabase("MyDatabase");
request.onsuccess = function(event) {
console.log("Database deleted successfully.");
};
request.onerror = function(event) {
console.log("Failed to delete the database:", event);
};
Can I import or export IndexedDB data?
Yes, you can manually export the data to a file or use third-party libraries for exporting and importing.
How secure is IndexedDB?
IndexedDB itself is client-side storage and does not provide built-in encryption.
However, you can manually encrypt sensitive data before storing it in IndexedDB to enhance security.
Does IndexedDB work offline?
Yes, IndexedDB is designed for offline-first applications.
Data is stored locally in the browser, allowing your application to work even without an internet connection.
What are object stores and indexes in IndexedDB?
Object stores are similar to tables in relational databases.
Indexes allow you to quickly search and retrieve data based on specific fields.
In summary, IndexedDB offers a robust solution for storing large and complex data client-side within web applications.
It is asynchronous, handles large amounts of structured data, and ensures data integrity through transactions.
Learning how to set up, manage, and optimize IndexedDB can significantly enhance your web development skills.
Shop more on Amazon