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.
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