IndexedDB
is a large-scale, NoSQL
storage system. It lets you store just about anything in the user's browser. In addition to the usual search, get, and put actions, IndexedDB
also supports transactions. Here is the definition of IndexedDB
on MDN:
"IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. This API uses indexes to enable high-performance searches of this data. While DOM Storage is useful for storing smaller amounts of data, it is less useful for storing larger amounts of structured data. IndexedDB provides a solution."
Each IndexedDB database is unique to an origin (typically, this is the site domain or subdomain), meaning it cannot access or be accessed by any other origin. Data storage limits are usually quite large if they exist at all, but different browsers handle limits and data eviction differently.
Fire up a code editor, and create an index.html
file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Promise to index my DB?</title>
</head>
<body>
<script>
// IndexedDB Code will go here
</script>
</body>
</html>
http-server
is a simple, zero-configuration command-line http server. It is powerful enough for production usage, but it's simple and hackable enough to be used for testing, local development, and learning.
$ npm install -g http-server
To run it type http-server
from the folder where you created the index.html
file.
IndexedDB
is one of the storage capabilities introduced into browsers over the years. It’s a key/value store (a NoSQL database) considered to be the definitive solution for storing data in browsers.
It’s an asynchronous API, which means that performing costly operations won’t block the UI thread providing a sloppy experience to users. It can store an indefinite amount of data, although once over a certain threshold the user is prompted to give the site higher limits.
It's supported in all modern browsers and supports transactions, versioning and gives a good performance.
This is the highest level of IndexedDB. It contains the object stores, which in turn contain the data you would like to persist. You can create multiple databases with whatever names you choose, but generally, there is one database per app.
A database is private to a domain, so any other site cannot access another website IndexedDB stores.
An object store is an individual bucket to store data. You can think of object stores as being similar to tables in traditional relational databases. Typically, there is one object store for each 'type' (not JavaScript data type) of data you are storing. For example, given an app that persists blog posts and user profiles, you could imagine two object stores. Unlike tables in traditional databases, the actual JavaScript data types of data within the store do not need to be consistent (for example, if there are three people in the 'people' object store, their age properties could be 53, 'twenty-five' and unknown ).
Each store usually contains a set of things, which can be
A store contains a number of items which have a unique key, which represents the way by which an object can be identified.
An Index is a kind of object store for organizing data in another object store (called the reference object store) by an individual property of the data. The index is used to retrieve records in the object store by this property. For example, if you're storing people, you may want to fetch them later by their name, age, or favorite animal.
A transaction is a wrapper around an operation, or group of operations, that ensures database integrity. If one of the actions within a transaction fails, none of them are applied and the database returns to the state it was in before the transaction began. All read or write operations in IndexedDB must be part of a transaction. This allows for atomic read-modify-write operations without worrying about other threads acting on the database at the same time.
An interaction with the database.
You can alter the stores using transactions, by performing add, edit and delete operations, and iterating over the items they contain.
A mechanism for iterating over multiple records in the database.
Since the advent of Promises in ES6, and the subsequent move of APIs to using promises, the IndexedDB API seems a bit old school.
While there’s nothing wrong in it, in this code-lab we will use the IndexedDB Promised Library
by Jake Archibald, which is a tiny layer on top of the IndexedDB API to make it easier to use.
idb
library using
$ npm install --save idb
And then include it in your page:
<script src="./node_modules/idb/build/idb.js"></script>
Before using the IndexedDB API, we need to check for support in the browser, even though it’s widely available, you never know which browser the user is using:
(() => {
'use strict';
if (!('indexedDB' in window)) {
console.warn('IndexedDB not supported');
return;
}
//...IndexedDB code
})()
Using idb.openDb()
which returns a promise that resolves to a DB.
const name = 'fe-guild';
const version = 1; //versions start at 1
idb.openDb(name, version, upgradeDB => {});
name
and version
behave as they do in indexedDB.open
.
upgradeCallback
is called if the version
is greater than the version last opened. It's similar to IDB's onupgradeneeded
. The callback receives an instance of UpgradeDB
An object store
is created or updated with the upgradeDB
callback, using the db.createObjectStore('storeName', options)
syntax:
const dbPromise = idb.openDb(name, version, upgradeDB => {
switch (upgradeDB.oldVersion) {
case 0:
// a placeholder case so that the switch block will
// execute when the database is first created
// (oldVersion is 0)
case 1:
console.log('Creating the products object store');
upgradeDB.createObjectStore('phones', {keyPath: 'id'});
}
});
createObjectStore()
accepts a second parameter that indicates the index key of the database. This is very useful when you store objects: put()
calls don’t need a second parameter, but can just take the value (an object) and the key will be mapped to the object property that has that name.
The index gives you a way to retrieve a value later by that specific key, and it must be unique (every item must have a different key)
A key can be set to auto increment, so you don’t need to keep track of it on the client code. If you don’t specify a key, IndexedDB will create it transparently for us:
upgradeDB.createObjectStore('phones', { autoIncrement: true });
but you can specify a specific field of object value to auto increment as well:
upgradeDB.createObjectStore('phones', {
keyPath: 'id',
autoIncrement: true
});
As a general rule, use auto increment if your values do not contain a unique key already (for example, an email address for users).
An index is a way to retrieve data from the object store. It’s defined along with the database creation in the idb.openDb()
callback in this way:
const dbPromise = idb.openDb('dogsdb', 1, upgradeDB => {
const dogs = upgradeDB.createObjectStore('dogs');
dogs.createIndex('name', 'name', { unique: false });
});
The unique
option determines if the index value should be unique, and no duplicate values are allowed to be added.
You can access an object store already created using the upgradeDB.transaction.objectStore()
method:
const dbPromise = idb.openDb('dogsdb', 1, upgradeDB => {
const dogs = upgradeDB.transaction.objectStore('dogs');
dogs.createIndex('name', 'name', { unique: false });
});
You can check if an object store already exists by calling the objectStoreNames()
method:
if (!upgradeDB.objectStoreNames.contains('store3')) {
upgradeDB.createObjectStore('store3');
}
You can use the put
method of the object store, but first, we need a reference to it, which we can get from upgradeDB.createObjectStore()
when we create it.
When using put
, the value is the first argument, and the key is the second. This is because if you specify keyPath
when creating the object store, you don’t need to enter the key name on every put() request, you can just write the value.
This populates greetings
store as soon as we create it:
idb.openDb('mydb', 1, upgradeDB => {
const greetings = upgradeDB.createObjectStore('greetings');
greetings.put('Hello world!', 'Hello');
})
To add items later down the road, you need to create a transaction
, that ensures database integrity (if an operation fails, all the operations in the transaction are rolled back, and the state goes back to a known state).
For that, use a reference to the dbPromise
object we got when calling idb.openDb()
, and run:
dbPromise.then(db => {
const tx = db.transaction('phones', 'readwrite');
const store = tx.objectStore('phones');
const phones = [
{
id: 'appl-xs',
brand: 'Apple',
model: 'iPhone XS',
color: 'Space Gray',
},
{
id: 'appl-6s',
brand: 'Apple',
model: 'iPhone 6S',
color: 'Rose Gold',
},
{
id: 'galaxy-s9',
brand: 'Samsung',
model: 'Galaxy S9',
color: 'Black',
}
];
return Promise.all(phones.map(phone=>{
console.log('Adding phone', phone);
return store.add(phone);
})).catch(error => {
tx.abort();
console.log(error);
}).then(() => console.log('All phones added successfully!'));
});
We update an item using the put()
method:
const tx = db.transaction('greetings', 'readwrite');
const store = tx.objectStore('greetings');
store.put('Yo!', 'Hello');
return tx.complete;
get()
dbPromise.then(db => {
const tx = db.transaction('phones');
const store = tx.objectStore('phones');
return store.get('appl-xs');
})
.then(phone => console.log(phone))
.catch(error => console.log(error));
getAll()
dbPromise.then(db => {
const tx = db.transaction('phones');
const store = tx.objectStore('phones');
return store.getAll();
})
.then(phones => console.log(phones))
.catch(error => console.log(error));
openCursor()
dbPromise.then(db => {
const tx = db.transaction('phones', 'readonly');
const store = tx.objectStore('phones');
return store.openCursor();
})
.then(function logItems(cursor) {
if (!cursor) { return; }
console.log('cursor is at:', cursor.key);
for (const field in cursor.value) {
console.log(cursor.value[field]);
}
return cursor.continue().then(logItems);
})
.then(()=> console.log('done!'));
const searchPhoneBetweenPrices = (lower, upper) => {
// check if the values are not undefined, null or NaN
let range;
if (lower > 0 && upper > 0) {
range = IDBKeyRange.bound(lower, upper);
} else if (lower === 0) {
range = IDBKeyRange.upperBound(upper);
} else {
range = IDBKeyRange.lowerBound(lower);
}
dbPromise.then(db => {
const tx = db.transaction('phones', 'readonly');
const store = tx.objectStore('phones');
const index = store.index('price');
return index.openCursor(range);
})
.then(function showRange(cursor) {
if (!cursor) { return; }
console.log(`cursor is at: ${cursor.value['model']} with price ${cursor.value['price']}`);
return cursor.continue().then(showRange);
})
.then(() => console.log('done!'));
};
searchPhoneBetweenPrices(300, 1000);
Deleting the database, an object store, and data
idb.delete('mydb').then(() => console.log('done'));
An object store can only be deleted in the callback when opening a DB, and that callback is only called if you specify a version higher than the one currently installed:
const dbPromise = idb.openDb('mydb', 2, (upgradeDB) => {
upgradeDB.deleteObjectStore('old_store');
})
const key = 232;
dbPromise.then((db) => {
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
store.delete(key);
return tx.complete
})
.then(() => console.log('Item deleted'));
You have learned the basics of working with IndexedDB.
Here is a checklist which breaks down the things we learned in this code lab.
getAll
methodSource code for this code lab can be found at https://github.com/The-Guide/fe-guild-pwa-indexed-db