Data storage is an important part of most web applications, from tracking user data to application data. With the rapid development of faster and more robust web applications, efficient client storage is needed to aid development.
Client-side storage on the web has evolved a lot over the years, from cookies used to store user data to the advent of WebSQL (currently deprecated), which allowed developers to store data in a SQL database in the browser, and in turn allowing users familiar with SQL to build robust applications easily.
IndexedDB is an alternative to WebSQL and provides more storage capacity than its previous counterpart. In this tutorial, we’ll explore how to use and set up IndexedDB for web application data storage and how to manipulate its data using the available API.
You can find a link to the public GitHub repo that contains the project we created in this article. We’ll cover the following:
Let’s get started!
IndexedDB is a low-level API for client-side storage. It is a full-blown, persistent NoSQL storage system available in the browser that allows for the storage of different types of data, such as:
IndexedDB can be used in various scenarios, such as caching, PWAs, and gaming, and also supports transactions. It is developed to suit the multiple needs of web apps effectively.
We won’t be doing any fancy setup, as IndexedDB runs natively on the web. First, create a new directory to house the project:
mkdir indexed-db && cd indexed-db
Now, we’ll create an index.html
file to view our application and an index.js
script file to store our application logic:
touch index.html index.js styles.css
To see the benefits of using this database and learn how to interact with the API, we’ll create a basic to-do application. We enable an Add feature to see how to save data to the database, another to view all to-dos, and a Remove feature to see the GET
and DELETE
functions of the API, respectively.
Open the index.html
created in the previous section and add the following code:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TODO APP</title> <script src="index.js" defer></script> <link href="style.css" rel="stylesheet"> </head> <body> <h1>TODO APP</h1> <section> <aside class="view"> <h2>TODOs</h2> <div class="todos"> <ol></ol> </div> </aside> <aside class="add"> <h2>Add Todo</h2> <form> <div> <label for="title">Todo title</label> <input id="title" type="text" required> </div> <div> <label for="desc">Todo description</label> <input id="desc" type="text" required> </div> <div> <button>Save</button> </div> </form> </aside> </section> </body> </html>
This creates the base structure of our web app. We do two main things here: first, we create a section to display/view all to-dos saved in the database, and second, we create a section for adding to-dos to the database.
Let’s also add some basic styling to the application. Open the styles.css
file and add the following:
html { font-family: sans-serif; } body { margin: 0 auto; max-width: 800px; } header, footer { background-color: blue; color: white; padding: 0 20px; } .add, .view { padding: 30px; width: 40%; } .add { background: #ebe6e6; } section { padding: 10px; background: #3182d4; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; } h1 { margin: 0; } ol { list-style-type: none; } div { margin-bottom: 10px; }
The index.js
file is the heart of the application, as it contains the logic for interacting between the app and IndexedDB.
First, we need to create the database; then, we can initialize it by creating an object store (similar to a table in SQL), which we‘ll use to store each item detail. Open the index.js
file and add the following logic to it:
let db; const openOrCreateDB = window.indexedDB.open('todo_db', 1); openOrCreateDB.addEventListener('error', () => console.error('Error opening DB')); openOrCreateDB.addEventListener('success', () => { console.log('Successfully opened DB'); db = openOrCreateDB.result; }); openOrCreateDB.addEventListener('upgradeneeded', init => { db = init.target.result; db.onerror = () => { console.error('Error loading database.'); }; const table = db.createObjectStore('todo_tb', { keyPath: 'id', autoIncrement:true }); table.createIndex('title', 'title', { unique: false }); table.createIndex('desc', 'desc', { unique: false }); });
As can be seen above, a database named todo_db
is created, and then an object store named todo_tb
is created with two indexes, title
and desc
. These indexes allow their values to be duplicated in the store, which is similar to creating a table in SQL and then creating two columns.
Next, to add the Save functionality, we proceed to retrieve the values entered into the form and then save them to the database:
const todos = document.querySelector('ol'); const form = document.querySelector('form'); const todoTitle = document.querySelector('#title'); const todoDesc = document.querySelector('#desc'); const submit = document.querySelector('button'); form.addEventListener('submit', addTodo); function addTodo(e) { e.preventDefault(); const newTodo = { title: todoTitle.value, body: todoDesc.value }; const transaction = db.transaction(['todo_tb'], 'readwrite'); const objectStore = transaction.objectStore('todo_tb'); const query = objectStore.add(newTodo); query.addEventListener('success', () => { todoTitle.value = ''; todoDesc.value = ''; }); transaction.addEventListener('complete', () => { showTodos(); }); transaction.addEventListener('error', () => console.log('Transaction error')); }
After adding the values to the store, the two form fields are emptied so the user can enter a new item to their list. We can update the view by calling the showTodos
method, which we’ll see in the next section.
To confirm if the Save to-do function worked, open and use your browser’s Inspect function. In Chrome, you can see IndexedDB under the Application tab, in Storage. As can be seen in the image below, we created the database and saved the first to-do to the todo_tb
object store:
To display the available to-dos when the user loads the page, and provide a view of previously added and removed to-dos, we’ll create a method called showTodos
:
function showTodos() { while (todos.firstChild) { todos.removeChild(todos.firstChild); } const objectStore = db.transaction('todo_tb').objectStore('todo_tb'); objectStore.openCursor().addEventListener('success', e => { const pointer = e.target.result; if(pointer) { const listItem = document.createElement('li'); const h3 = document.createElement('h3'); const pg = document.createElement('p'); listItem.appendChild(h3); listItem.appendChild(pg); todos.appendChild(listItem); h3.textContent = pointer.value.title; pg.textContent = pointer.value.body; listItem.setAttribute('data-id', pointer.value.id); const deleteBtn = document.createElement('button'); listItem.appendChild(deleteBtn); deleteBtn.textContent = 'Remove'; deleteBtn.addEventListener('click', deleteItem); pointer.continue(); } else { if(!todos.firstChild) { const listItem = document.createElement('li'); listItem.textContent = 'No Todo.' todos.appendChild(listItem); } console.log('Todos all shown'); } }); }
This method gets the to-dos from the store, loops through each item, and creates an HTML element for each. It appends the item to the ol
list element on the webpage and passes the id
of each to-do into a data attribute called data-id
. We’ll use this unique ID later, when we cover the deleteItem
function, to identify each to-do when we need to remove it from the store.
To fetch the to-dos on page load, modify the openOrCreateDB
success event listener to this:
openOrCreateDB.addEventListener('success', () => { console.log('Successfully opened DB'); db = openOrCreateDB.result; showTodos(); });
Finally, let’s test the DELETE
API for this database and create a Delete function for our to-do list app:
function deleteItem(e) { const todoId = Number(e.target.parentNode.getAttribute('data-id')); const transaction = db.transaction(['todo_tb'], 'readwrite'); const objectStore = transaction.objectStore('todo_tb'); objectStore.delete(todoId); transaction.addEventListener('complete', () => { e.target.parentNode.parentNode.removeChild(e.target.parentNode); alert(`Todo with id of ${todoId} deleted`) console.log(`Todo:${todoId} deleted.`); if(!todos.firstChild) { const listItem = document.createElement('li'); listItem.textContent = 'No Todo.'; todos.appendChild(listItem); } }); transaction.addEventListener('error', () => console.log('Transaction error')); }
This deletes the particular to-do using the unique ID passed to the method and removes the element from the webpage. Once it deletes the last to-do item in the store, it shows a “no to-dos” message in the place of the to-do list.
To confirm that the to-do has been deleted from the database, proceed to inspect the webpage and click the Application tab. As can be seen, the todo_tb
object store now contains no items:
The final web application looks like this:
IndexedDB also allows developers to increment the database version. When you open the database, specify your desired version number:
window.indexedDB.open('todo_db', 1);
If the database doesn’t exist, it will be created with the specified version. If the database already exists, the version number is checked.
If the version number specified during the open method call is higher than the existing version, a version change event is triggered via the onUpgradeNeeded
event. This event allows you to perform database schema changes or data migrations.
A point to note here is deleting a previous object store to add new options when creating a new store would also delete all other data in the old store. Take care to read the old content out and save it somewhere else before upgrading the database.
Since IndexedDB relies on the client’s web browser, it is typically more suitable for individual users or smaller-scale applications. Though it can handle a significant amount of data, there are certain considerations to keep in mind when using IndexedDB in large-scale apps or apps used by multiple people.
IndexedDB operates within a web browser, which means it is limited to the capabilities and resources of the client-side environment. It may not scale well for scenarios that require handling large numbers of simultaneous users or extremely high throughput.
Different web browsers impose limits on the maximum amount of data that can be stored in IndexedDB. These limits vary across browsers and can range from a few megabytes to several hundred megabytes.
It is greatly important to be aware of these limitations and design the application accordingly. Once your data storage runs out, you won’t be able to store new data in the database, as you would trigger a QuotaExceededError
error.
IndexedDB doesn’t provide inbuilt mechanisms for handling data synchronization between clients or handling conflicts in a distributed environment. You would need to implement custom synchronization logic to handle these scenarios. Maintaining data consistency and synchronization across different instances of the application becomes complex.
Hence, for larger-scale applications or applications used by multiple people, it is more efficient to use server-side databases or cloud-based storage solutions.
In this article, we learned about IndexedDB, a database on the web, and how to interact with it to store web application data using JavaScript.
Hopefully, you enjoyed this article and have learned a new way of managing your application data locally on the web. Thanks for reading!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
Build a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend.
console.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
One Reply to "A complete guide to using IndexedDB"
Immensely grateful!! – Thank you for sharing.