How to use IndexedDB to store images and other files in your browser
Tags: javascript, webdev, database, tutorial
Published: 2023-10-04 08:57:09

> TL:DR Take me to the [Github Repos](https://tq-bit.github.io/quota-check/) or the [Live Demo](https://tq-bit.github.io/indexedb-file-storage/)

---

> ⚠️ **A note on browser behavior**  
> Persistent storage implementation largely depends on the browser. For the sake of ease, the project for this article does not implement user-preference checks, [such as asking for storage permission](https://github.com/tq-bit/quota-check/blob/main/index.js#L227). It's up to you to implement them yourself and ensure browser compatibility.

About IndexedDB
---------------

### IndexedDB vs. Web Storage

When compared to other [web storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) methods, IndexedDB comes with a few extra perks. It:

*   works asynchronously, does not block the tab's main thread
*   uses [transactions](https://en.wikipedia.org/wiki/Database_transaction) to ensure data integrity
*   allows for [indexing](https://en.wikipedia.org/wiki/Database_index) and faster querying
*   has increased storage capacity (depending on the browser)

### Browser compatibility and usability

At the time of writing this article:

*   IndexedDB ships with all [modern browsers](https://caniuse.com/?search=indexedDB)
*   One of the [core features to estimate storage](https://caniuse.com/?search=StorageManager%20API%3A%20estimate) quota was recently added with Safari 17
*   A relatively new one, [usage details,](https://caniuse.com/?search=usageDetails) is still missing in several browsers

### Storage quota and persistence

Browsers reserve space on your device's drive for persistence. They define a quota that governs the total amount of data to store.

> Important: Quota does not only include IndexedDB, but also other web persistence APIs, caches and ServiceWorker registrations. Consider quota limits carefully when designing local storage strategies

The easiest way to find out how much storage your browser reserves is by calling the 'estimate' method:

```js 
(async function() {
    const estimate = await navigator.storage.estimate();
    console.log(estimate)
})()

```

Doing this on the google.com domain gives the following result:

![](https://blog.q-bit.me/content/images/2023/09/image.png)

The values' unit is shown in bytes. To determine the remaining MB/GB, divide the quota and usage by:

*   quota / 1024 for KB (= 2097152 KB)
*   quota / 1024² for MB  (= 2048 MB)
*   quota / 1024³ for GB (= 2GB)

> In a nutshell, this means the origin 'google.com' has a total of 2GB quota on my hard drive available. It currently uses around 9kb of them.

Use cases
---------

### Offline-first scenarios

IndexedDB is particularly useful for storing data in [Progressive Web Apps](https://web.dev/progressive-web-apps/). Compared to the service worker's cache, data stored in IndexedDB remains easily accessible and resilient to data integrity problems during synchronization.

### Client caching strategies

A few months ago, I reported to a German government institution. They requested many documents, preferably in PDF, over a portal platform. I had to upload each file in a separate file form field.

After ten-ish minutes, my form submission failed due to a server problem. The form data was deleted from the browser, including my documents.

> It would've been a good idea to store these documents somewhere on the client before sending them to the server so I would not have to upload everything all over

But they weren't. So rinse and repeat from the beginning.

### Implement data privacy

There are a few good reasons to consider local storage for privacy reasons. The most prominent are probably laws like the GDRP in Europe. For such it comes in handy if entered data never leaves the respective computer. IndexedDB provides a lot of relevant methods to handle everything your user needs.

Possible pitfalls
-----------------

### Accidental deletion by the user

Since the browser stores the data, they're easily deleted by incautious usage. When a user requests the browser to delete their data, this could include data stored by IndexedDB.

### Data loss due to exceeded storage quota

Modern browsers reserve a lot of storage space for browsers. It is possible for data loss to occur once the required exceeds available storage space.

### Unexpected browser behavior

With that, I don't mean the browser behaves in an unexpected manner. Much rather, its behavior is correct, you just wouldn't expect it. A prominent example is Apple's [ITP policy for Safari](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/), which removes stale data (including IndexedDB!) after seven days without interaction.

Implementation
--------------

So far, we've established a general understanding of the 'what's and 'why's. Let's dive into how to use IndexedDB next.

> There's a GitHub repository that mirrors this article's content. If you'd like to follow along, start with its branch [01-start](https://github.com/tq-bit/indexedb-file-storage/tree/01-start)

### Create the database and object-storage

> Note: Check out branch [02-init-db](https://github.com/tq-bit/indexedb-file-storage/tree/02-init-db) for reference

Let's start by creating the database to save images. The process of doing so involves the following steps:

1.  Request the database using its name and version
2.  Create a store (or many stores) in which to save images
3.  Optional: Set up an index to query the store
4.  Return the created db instance for further usage

#### Request the database

We can use the 'IDBFactory.open(name, version)' method to request the database to open. If the version of the database you request does not exist yet, the browser will create it for you.

```js 
const request = indexedDB.open(dbName, dbVersion);

```

#### Handle the database request's events

Once the operation completes, the request emits three events:

1.  'onerror' means: The database request failed
2.  'onsuccess' means: The database request was successful
3.  'onuptedeended' means: A new version of the database was created during the request. This event also fires when the database initializes for the first time

```js 
request.onerror = (event) => { /* handle the error event */ }
request.onsuccess = (event) => { /* handle the success event */ }
request.onupgradeneeded = (event) => { /* handle the upgrade event */ }

```

#### Add the code to initialize the database

In a nutshell, this is what's going to happen:

![](https://blog.q-bit.me/content/images/2023/10/Drawing-2023-10-03-15.40.30.excalidraw-1.png)

So go ahead and add the following to the 'initIndexedDb' - function:

```js 
const initIndexedDb = (dbName, stores) => {
	return new Promise((resolve, reject) => {
		const request = indexedDB.open(dbName, dbVersion);
		request.onerror = (event) => {
			reject(event.target.error);
		};
		request.onsuccess = (event) => {
			resolve(event.target.result);
		};
		request.onupgradeneeded = (event) => {
			stores.forEach((store) => {
				const objectStore = event.target.result.createObjectStore(store.name, {
					keyPath: store.keyPath,
				});
				objectStore.createIndex(store.keyPath, store.keyPath, { unique: true });
			});
		};
	});
};

```

#### Create a new database instance

The function is called as part of the window.load - event. It will set the global 'db' - variable to the created database instance.

```js 
// Create some constants for the store's name and key
const storeName = 'localFiles';
const storeKey = 'fileName';

// Create a db - variable to access the database after creation
let db = null;

// The function you just created
const initIndexedDb = (dbName, stores) => { /* ... */}

// Attach IndexedDB - creation to the window.onload - event
window.addEventListener('load', async () => {
	db = await initIndexedDb('my-db', [{ name: storeName, keyPath: storeKey }]);
})

```

#### Validate the database's creation

You can also see the new database in your browser's dev tools:

![](https://blog.q-bit.me/content/images/2023/10/image.png)

### Add a new image and show it in the UI

> Note: Check out branch [03-add-images](https://github.com/tq-bit/indexedb-file-storage/tree/03-add-images) for reference

Now that our database is in place, we can start populating it. IndexedDB stores data in key-value pairs. In the case of this implementation, keys are equal to the file's name. We'll use them to engineer the search feature later, too.

#### Create a new file-entry

Let's start by adding to the 'handleSubmit' function. It fires once the user clicks on the 'Add to IndexedDB' - button and does the following:

1.  [Grab the file's value from the input field](https://github.com/tq-bit/indexedb-file-storage/blob/main/index.js#L99)
2.  Create a read-write-transaction
3.  Add the file to the object store
4.  Re-render the gallery cards below the form
5.  Update the storage quota in the UI

Let's add the following code the the 'handleSubmit' function:

```js 
const handleSubmit = async (ev) => {
	ev.preventDefault();
	const file = await getFileFromInput();
	const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
	store.add(file);

	store.transaction.oncomplete = () => {
		clearGalleryImages();
		renderAvailableImagesFromDb();
		renderStorageQuotaInfo();
	};
};

```

#### Re-render the image gallery

To access the new file, the previous transaction must complete. We'll then use a [cursor](https://en.wikipedia.org/wiki/Cursor_(databases)) to iterate over all database entries and render them into the UI. A cursor will provide us with the database's respective record's key and value.

Add the following code to the 'renderAvailableImagesFromDb' function:

```js 
const renderAvailableImagesFromDb = () => {
	db.transaction(storeName, 'readonly').objectStore(storeName).openCursor().onsuccess = (event) => {
		const cursor = event.target.result;
		if (cursor) {
			renderGalleryColumn(cursor);
			cursor.continue();
		}
	};
};

```

Now you can start adding files to your database. Check out your devtools under 'Application' to see them getting stored in your browser.

![](https://blog.q-bit.me/content/images/2023/10/image-1.png)

### Execute a search query

> Note: Check out branch [04-search](https://github.com/tq-bit/indexedb-file-storage/tree/04-search) for reference

Querying for files works in a similar manner. Instead of rendering all available images, we'll only render those relevant to the search term.

Add the following code to your 'handleSearch' function:

```js 
const handleSearch = async (ev) => {
	ev.preventDefault();
	clearGalleryImages();
	const searchInput = document.getElementById('search').value;
	db.transaction(storeName, 'readonly').objectStore(storeName).openCursor().onsuccess = (event) => {
		const cursor = event.target.result;
		if (cursor) {
			if (cursor.value[storeKey].toLowerCase().includes(searchInput.toLowerCase())) {
				renderGalleryColumn(cursor);
			}
			cursor.continue();
		}
	};
};

```

### Delete images

> Note: Check out branch [05-delete](https://github.com/tq-bit/indexedb-file-storage/tree/05-delete) for reference

Let's wrap this article up by adding two delete functions

*   One to delete a single image
*   One to empty the whole object-store

#### Delete a single image

To delete a single entry, we need its key. In this project, it is already bound to the delete button's click event.

```js 
const renderGalleryColumn = (cursor) => {
    // ...
	deleteButton.addEventListener('click', () => {
		deleteImageFromIndexedDb(cursor.value[storeKey]);
	})
    // ...
}

```

All that's left to do is to:

*   Call the 'delete(key)' method from the object store
*   Re-render the remaining gallery entries once the transaction completes

Let's add the following code to the 'deleteImageFromIndexedDb' function:

```js 
const deleteImageFromIndexedDb = (storeKey) => {
	const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
	store.delete(storeKey);
	store.transaction.oncomplete = async () => {
		clearGalleryImages();
		renderAvailableImagesFromDb();
		await renderStorageQuotaInfo();
	};
};

```

#### Delete all images

Finally, you can delete all entries from your database by calling the store 'clear()' - method:

```js 
const clearEntriesFromIndexedDb = () => {
	const store = db.transaction(storeName, 'readwrite').objectStore(storeName);

	store.clear();
	clearGalleryImages();

	store.transaction.oncomplete = () => {
		renderStorageQuotaInfo();
	};
};

```

Resources and further reading
-----------------------------

*   [MDN docs on IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
*   [Dexie.js IndexedDB wrapper library](https://dexie.org/)
*   [Article on web.dev to compare different methods for 'Storage for the web'](https://web.dev/storage-for-the-web/)