Easy Offline Storage with idb and the IndexedDB API

I really like the JavaScript library idb, a minimal wrapper for the IndexedDB API. I've used it in several web applications to provide offline capabilities.

The idb library makes it easy to store data on your device and retrieve it later via a Promise. It also includes great TypeScript types and interfaces that support creating generic logic for interacting with your offline storage.

A woman sitting on the floor surrounded by cardboard boxes. Photo: © RDNE Stock project / pexels.com

Let's take a look at the basics of setting up a database with idb. Then I'll provide some code examples of reusable generic methods for storing and retrieving data.

Setting up the database

First of all, you'll need to define the schema for the data you want to store. Here's a basic example:

import { DBSchema } from "idb"; export interface MyAwesomeDB extends DBSchema { images: { value: MyImageData; key: string; }; videos: { value: MyVideoData; key: string; }; }

I recommend you bundle the DB operations in their own class. This class only serves the specific purpose of interacting with the database, making your code easier to read and understand. In an Angular application, I would create an injectable service class like this:

import { IDBPDatabase, openDB } from "idb"; @Injectable() export class OfflineStorageService { private _databaseVersion = 1; // The promise returned when opening the IndexedDB with openDB() method. private _myDatabase$: Promise<IDBPDatabase<MyAwesomeDB>> | null = null; public init(): void { this._myDatabase$ = openDB<MyAwesomeDB>( "my-awesome-database", this._databaseVersion, { // Initialize objects in database if opened for the first time. upgrade: (database, oldVersion) => { ... }, }, ); // Handle success and error case. this._myDatabase$.then( () => console.log("IndexedDB was successfully opened.""), () => this.onOpeningDatabaseFailed(), ); } }

Storing data

Let's assume you want to store several items in a specific object store of your database. Usually, you'll open a transaction with readwrite access and then store the items. For this operation, you can define a generic method with the parameters storeName and items:

import { StoreNames, StoreValue } from "idb"; // Stores all items in the object store named "storeName". private storeAllItemsInObjectStore<Name extends StoreNames<MyAwesomeDB>>(storeName: Name, items: StoreValue<MyAwesomeDB, Name>[]): void { this._myDatabase$?.then((database) => { const transaction = database.transaction(storeName, "readwrite"); items.forEach((item) => transaction.store.add(item)); }); }

The method uses the types StoreNames and StoreValue from the idb library. They make sure that the storeName parameter passed to the method is actually part of the database and that the items have the expected data structure. Here's an example of using this generic method:

public storeImages(items: MyImageData[]): void { this.storeAllItemsInObjectStore("images", items); }

Retrieving data

When retrieving data from the local database, I prefer turning the native Promise into an Observable. This makes it easier to integrate the offline storage into an observable based application state logic (e.g., NgRx store). Here's a generic method that returns a specific item from an object store wrapped in an observable:

import { StoreKey, StoreNames, StoreValue } from "idb"; import { Observable, catchError, from, of, switchMap } from "rxjs"; // Retrieves the item identified via "itemKey" from the object store named "storeName". Resolves with undefined if no match is found, the database is unavailable, or there's an error. private getItemFromObjectStore<Name extends StoreNames<MyAwesomeDB>>( storeName: Name, itemKey: StoreKey<MyAwesomeDB, Name>, ): Observable<StoreValue<MyAwesomeDB, Name> | undefined> { if (this._myDatabase$ === null) { return of(undefined); } return from(this._myDatabase$).pipe( switchMap((database) => from(database.get(storeName, itemKey))), catchError((error) => { console.error(error); return of(undefined); }), ); }

You can use the generic method like this:

public getImage(imageId: string): Observable<MyImageData | undefined> { return this.getItemFromObjectStore("images", imageId); }

Have fun using IndexedDB and the idb library!

Posted on