Understanding IndexedDB. The complete guide.
Table of contents
Before we start
I have been working with IndexedDB for more than 3 years. When I just started, I thought, huh, it's similar to MongoDB, easy-peasy. Quite fast I understood that:
The native API is not that easy to use, you need a handy wrapper like Dexie or idb with Promises from Jake Archibald
It's quite easy to make IndexedDB slow, especially if you're using Dexie, but that's a separate story.
Transactions are fragile
and many other things
I used native iDB API, prepared the typing for iDB in flow (yup, I'm working in Meta), worked with Dexie and even worked on my own ORM. I kept finding some tricky yet interesting insights about IndexedDB.
This article is a complete "What do I know about IndexedDB" guide.
Get your coffee, bookmark the page, share / retweet!
❗️ All the examples are available in this codesandbox or github repo.
IndexedDB:
is NoSQL and represents key-value storage DB;
don't have fixed schema for ObjectStores:
- You can save something like:
1: {foo: 'bar'}, 2: {hello: 123}, 'world': {bar: {baz: ['hello', 123', 'world']}}
where 1,2, and 'world' are primary keys;
- You can save something like:
has indexed access. If you want to find the data by key, the key should be defined beforehand. If you try to filter data by non-indexed field you will receive an error:
- Note: you can read all the records or iterate through all the records but it will be extremely slow (O(n));
is transaction based. You cannot access (neither read nor write) the DB outside the transaction;
is async by design. Original IndexedDB API is callback-based, however, there are tons of promise-based wrappers;
supports the work with blobs, binary formats (ArrayBuffers and so on);
has LevelDB as a backend for iDB in Chrome and hence for V8-based browsers.
The structure of IndexedDB:
IndexedDB consists of:
Database: the top level of IndexedDB. Each Database contains:
Name: unique name of the DB for the definite domain;
Version: DB versions are used for DB migrations. DB migration is a special type of transaction, named "version change transaction";
Object Store: a "table" in your Database. An object store consists of a list of records, addressed by your key path.
In addition to your key, you can have several indexes. Every Index will point out to your primary key in the object store (to avoid data duplicity)
Keys: as ObjectStore is key-value storage, keys are used as a unique path to your values (similar to primary key).
Single key:Compound keyPath:
Indexes: each object store can have a list of many indexes associated with that store. Index refers to a list of fields of the objects in the store. Only the first level of objects is used.
Indexes are used as a reference to a key, which allows IndexedDB to address the record itself.
Index can be:Compound: consists of several fields
Single: indexes single field in ObjectStore
Unique: repetition is prohibited
MultiEntry: support array indexation
IndexedDB cheatsheet schema:
Link: https://excalidraw.com/#json=fCE1ViGCCh-cmWt3vwLUw,6ruYd_5L1F-_dehB7XQAkQ
How to: create database
There is no separate API just to "create" a database. Everything starts with a connection. indexedDB.open
creates a connection to the database. If there is no such database, it will be created.
indexedDB.open
receives:
DB name: required field to identify the DB you want to be connected to
version: the version of the DB. This field is optional. If omitted, you still will be able to understand the current DB version and establish a connection.
To handle the result of the .open
you should subscribe to the following events:
upgradeneeded
: happens when the provided DB version is higher than existing, and therefore DB update is required. The handler of the event is a version change transaction, which can create/delete objectStores and indexes;success
: DB version change transaction is successful (if any) and the connection is established. In that case, you can access the DB throughrequest.result
;error
: connection attempt / version change transaction throws an error;blocked
: another code / tab maintains a connection to the DB with a smaller version number.
const request = indexedDB.open(DB_NAME: string, VERSION: number);
// Version change transaction
request.onupgradeneeded = () => {
// DB connection for the transaction operation
const db: IDBDatabase = request.result;
// IDB transaction ref (version change transaction)
const transaction: IDBTransaction = event.currentTarget.transaction;
};
request.onsuccess = () => {
// establisehed connection to IDBDatabase
const db: IDBDatabase = request.result;
db.version // contains the VERSION of the DB
};
request.onerror = (errorEvent) => {
// Error during updating the db
};
request.onblocked = () => {
// DB is blocked
// (another codepath / tab maintains connection to old DB version)
};
Most of the real-life use cases open the database with version, as you want to ensure your code is up-to-date and has all keys, objectStores and indexes you need.
Async-await version of opening the connection to the DB:
const db: IDBDatabase = await openConnection('myDatabaseName', 4, (db) => {
// version change transaction code goes here
});
function openConnection(dbName: string, version: number, upgradeTxn: (db: IDBDatabase) => void): Promise<IDBDatabase> {
return new Promise((resolve,reject) => {
const request = indexedDB.open(dbName, version);
// Version change transaction
request.onupgradeneeded = function (event) {
upgradeTxn(request.result);
}
// Update completed successfully, DB connection is established
request.onsuccess = () => {
resolve(request.result);
};
// Something goes wrong during connection opening and / or
// during version upgrade
request.onerror = (errorEvent) => {
console.error('Cannot open db');
reject(errorEvent);
};
// Stands for blocked DB
request.onblocked = () => {
console.warn('Db is blocked');
reject('db is blocked');
};
});
}
When the Database is created for the first time, we call .open
with database name and (usually) with the 1
version of the DB. Note: you can use any number as a version of the database. As the DB doesn't exist yet, the new database will be created and your code will call a version change transaction
.
📝 Version change transaction is a special type of transaction that can be accessed only when you create or upgrade the database version.
📝 Version change transaction has exclusive access to:
objectStores manipulation: create / delete
keys definition: simple / compound / autoincremental
indexes manipulation: create / delete / etc.
Once the transaction is completed, DB will have the new version set.
If there is an error during a transaction, the error event will be thrown, which can be caught using request.onerror
handler.
If there is another connection to the same DB is maintained with smaller DB version number, the .open
request will get blocked
event thrown.
Overall schema of indexedDB.open
:
Example of iDB creation with one objectStore and one additional index:
function openConnection(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open("Understanding_IndexedDB", 1);
// Version change transaction
request.onupgradeneeded = function () {
db = request.result; // IDBDatabase
// Only version change transaction has access to create/delete object stores
const namesObjectStore = db.createObjectStore("name-store", {
keyPath: "id",
// We can create auto-increment primary key
autoIncrement: true,
});
// Only version change transaction has access to create/delete indexes
namesObjectStore.createIndex("index[name]", "name", { unique: false });
};
// Will be called once onupgradeneeded is completed or
// when the DB already has correct version
request.onsuccess = () => {
LOG`DB: ${TEST_DB_NAME} created`;
resolve(request.result); // return IDBDatabase
};
// Error in transaction
request.onerror = (e) => {
LOG`DB: ${TEST_DB_NAME} cannot be created: ${e}`;
reject(e);
};
// Current DB is blocked
request.onblocked = (e) => {
LOG`DB: ${TEST_DB_NAME} is blocked: ${e}`;
reject(e);
};
});
}
📝: The versioning for indexedDB.open
can only be non-decreasing. If the version passed to indexedDB.open
is less than the real DB version, it would throw an error.
It's extremely important!
📝 if you accidentally push a version, which breaks client processing, you cannot roll back changes. You have to do the fix forward. IndexedDB doesn't have "version rollback" transactions.
How to: create ObjectStore
ObjectStore can be created / deleted inside version change transaction only.
createObjectStore
creates a new objectStore in your Database.
idbDatabase.createObjectStore(
OBJECT_STORE_NAME: string,
KEY: {
keyPath: string | Array<string>,
autoIncrement: boolean = false // default
}
);
As values are flexible, the minimal variant of createObjectStore
receives only the object store name:
idbDatabase.createObjectStore('myNewObjectStore');
In that case, objects in the store won't have the key presented and will use keys separated from the values. It is also called out-of-line
keys.
The second argument of createObjectStore
allows you to define the key for the object store, and it's declared by an object with 2 fields:
keyPath
: string or array of strings, which represents the "primary key" for key-value storage.- The key has to be unique. e.g. if you have
keyPath
defined for a person'sname
, you are about to receive an error as soon as you try to persist 2 persons with the same name.
- The key has to be unique. e.g. if you have
autoIncrement
: a flag that shows if the key is auto-incremental or not. The default isfalse
.
const namesObjectStore = db.createObjectStore("name-store", {
keyPath: "id",
autoIncrement: true,
});
You can use multiple fields as a keyPath:
db.createObjectStore("address-store", {
// Complex keypath based on 6 keys:
// NOTE: don't do keypath based on address, use it as index for production code ;)
keyPath: ["country", "city", "street", "house", "flat", "zip"],
});
📝 The order of fields in KeyPath is important. That's exactly how the data will be presented in your DB.
To delete an object store, call deleteObjectStore
:
idbDatabase.deleteObjectStore('myNewObjectStore');
How to: create an index
📝 Index is an additional way of accessing values in the object store. Technically index references the key, which refers to the value in the object store.
📝 Indexes creation / deletion can happen inside Version change transaction only.
To create an index you should have a reference to the object store:
objectStore.createIndex(
UNIQUE_INDEX_NAME: string,
KEY_PATH: string | Array<string>,
OPTIONS: {
// Shows if duplicate records for this index are allowed
unique: boolean = false,
// Index can resolve entries inside array:
multiEntry: boolean = false,
}
);
The following code creates an index based on the name
field. The index is marked as non-unique, and it might be omitted, as unique: false
is the default value:
// Create an object store with out-of-line key:
const namesObjectStore = db.createObjectStore("name-store");
// Add index:
namesObjectStore.createIndex("index[name]", "name", { unique: false });
To create an index based on several fields we use Array<string>
as a keyPath:
event.currentTarget.transaction
.objectStore("name-store")
.createIndex("index[name + lastName]", ["name", "lastName"], {
unique: false,
});
📝 The order of fields in KeyPath is important. That's exactly how the data will be indexed in your DB.
📝 sometimes, you may want to store an array with strings or numbers. And you might want to index the data based on this array. To achieve this, you can use an index with multiEntry
option:
// Call inside onupgradeneeded callback:
event.currentTarget.transaction
// select object store:
.objectStore("name-store")
// citizenship field is Array<String>, like: ['USA', 'UK', ...]
.createIndex("index[citizenship]", "citizenship", {
multiEntry: true,
});
nameStore.add({ name: "John", lastName: "Doe", citizenship: ["USA"] });
nameStore.add({
name: "Alice",
lastName: "Smith",
citizenship: ["USA", "UK"],
});
nameStore.add({
name: "Bob",
lastName: "Johnson",
citizenship: ["USA", "Germany"],
});
nameStore.add({
name: "Eva",
lastName: "Anderson",
citizenship: ["Greece"],
});
If we request data based on index[citizenship]
field with USA
as a request, we will receive John
, Alice
, and Bob
, but not Eva
.
How to: IDBTransaction
There are 3 types of IDBTransactions:
📝
readonly
: only read requests. It's possible to have multiple readonly transactions opened to the same objectStore in the same time;📝
readwrite
: requires write access to the object store. You can have only 1 active transaction per time for the particular objectStore📝 Version change: happens exclusively when the connection to the DB gets opened and the requested version is higher than the current DB version. This type of transaction has access to objectStores, indexes creation / deletion.
To create a transaction, you should have an active connection to the Database:
const transaction: IDBTransaction = (db: IDBDatabase)
.transaction(Array<ObjectStoreName>, "readwrite" | "readonly");
📝 Transaction gives you access to ObjectStore (depending on transaction permissions to read / write operations):
// Open DB connection (if we didn't do it before)
const db: IDBDatabase = await openConnection("myTestDb", 4);
// Open readwrite transaction
const txn = db.transaction(["name-store", "address-store"], "readwrite");
// Request name-store objectStore
const nameStore: IDBObjectStore = txn.objectStore("name-store");
📝 Transaction is the only way to get/set the data:
const idbRequest: IDBRequest = nameStore.add({
name: "Alice",
lastName: "Smith",
citizenship: ["USA", "UK"],
});
📝 You have direct control on when to abort (rollback) the transaction or when to commit it.
📝 txn.commit
ends current transaction and persists the data from the IDBRequests you had:
// Open DB connection (if we didn't do it before)
const db: IDBDatabase = await openConnection("myTestDb", 4);
// ...
// Open readwrite transaction
const txn = db.transaction(["name-store", "address-store"], "readwrite");
// Request name-store objectStore
const nameStore: IDBObjectStore = txn.objectStore("name-store");
const idbRequest: IDBRequest = nameStore.add({
name: "Alice",
lastName: "Smith",
citizenship: ["USA", "UK"],
});
// nameStore got new record Alice Smith
idbRequest.onsuccess = (e) => {
console.log("idb request success");
txn.commit(); // Commits the current transaction
}
txn.oncomplete = (e) = {
console.log("idb transaction is done");
}
txn.onabort = (e) = {
console.log("idb transaction is aborted");
}
// Alice Smith is written to the db
// output:
// idb request success
// idb transaction is done
📝 txn.abort()
closes the transaction and rolls back all the changes:
// Open DB connection (if we didn't do it before)
const db: IDBDatabase = await openConnection("myTestDb", 4);
// ...
// Open readwrite transaction
const txn = db.transaction(["name-store", "address-store"], "readwrite");
// Request name-store objectStore
const nameStore: IDBObjectStore = txn.objectStore("name-store");
const idbRequest: IDBRequest = nameStore.add({
name: "Alice",
lastName: "Smith",
citizenship: ["USA", "UK"],
});
// nameStore got new record Alice Smith
idbRequest.onsuccess = (e) => {
console.log("idb request success");
txn.commit(); // Commits the current transaction
}
txn.oncomplete = (e) = {
console.log("idb transaction is done");
}
txn.onabort = (e) = {
console.log("idb transaction is aborted");
}
// Alice Smith is NOT written to the db (rolled back)
// output:
// idb request success
// idb transaction is aborted
Transaction concurrency:
📝 Transactions are queued for the execution once indexedDB.transaction
is requested.
📝 readwrite
transactions for the same objectStore are executed consequently with the the order defined by .transaction
requests:
const db: IDBDatabase = await openConnection("myTestDb", 4);
// ...
// 1st txn
const txn = db.transaction(["name-store", "address-store"], "readwrite");
// 2nd txn
const txn2 = db.transaction(["name-store"], "readwrite");
const nameStore2: IDBObjectStore = txn2.objectStore("name-store");
const idbRequest2: IDBRequest = nameStore2.add({name: "Alice 2"});
const nameStore: IDBObjectStore = txn.objectStore("name-store");
const idbRequest: IDBRequest = nameStore.add({name: "Alice"});
// Alice will be written first
// Alice2 will be written after the first transaction gets completed
📝 readonly
transaction can be executed concurrently and the order is not determined in that case:
// 1st txn
const txn = db.transaction(["name-store", "address-store"], "readonly");
// 2nd txn
const txn2 = db.transaction(["name-store", "address-store"], "readonly");
const nameStore2: IDBObjectStore = txn2.objectStore("name-store");
const nameStore: IDBObjectStore = txn.objectStore("name-store");
const request2 = nameStore2.getAll();
const request = nameStore.getAll();
txn2.oncomplete = (e) => {
LOG`The 2nd transaction is done`;
LOG`${request2.result}`;
LOG`First request state: ${request.readyState}`;
};
txn2.onabort = (e) => {
LOG`idb transaction is aborted`;
};
txn.oncomplete = (e) => {
LOG`The 1st transaction is done`;
LOG`${request.result}`;
LOG`Second request state: ${request2.readyState}`;
};
txn.onabort = (e) => {
LOG`idb transaction is aborted`;
};
📝 If you have 2 transactions: readonly and readwrite, they will be executed in order of .transaction
.
📝 Several readwrite transactions for the different objectStores are executed concurrently.
const txn = db.transaction(["address-store"], "readwrite");
const txn2 = db.transaction(["name-store"], "readwrite");
const addressStore: IDBObjectStore = txn.objectStore("address-store");
const nameStore2: IDBObjectStore = txn2.objectStore("name-store");
nameStore2.add({name: "Alice 2"});
const clearRequest = addressStore.clear();
clearRequest.onsuccess = () => {
addAddresses(addressStore); // adds 10+ records to the db
};
txn2.oncomplete = (e) => {
LOG`name-store, 2nd transaction is done`;
};
txn.oncomplete = (e) => {
LOG`address-store, 1st transaction is done`;
};
// txn2 will be completed sooner, than txn in most of the cases,
// despite we start txn first. However the order is not strict here.
// (just becasue, txn2 has too many things to do)
📝 Rules above are scaled the same way if multiple tabs are opened for the same domain (only one readwrite transaction to a single objectStore, and etc).
Transaction lifetime:
📝 Transaction is maintained by browser until one of these events:
transaction.commit()
is called. It will end transaction and commit all the changes. Any pendingIDBRequest
will be completed withsuccess
,IDBTransaction
will be completed withcomplete
eventNo any pending
IDBRequest
/success
callbacks in the next macro task call of the browser event loop.In that case, as no other work is planned, transaction is treated as successfully completed and IndexedDB behaviour is the same, as per
.commit()
call.IMPORTANT: transaction still will be maintained in case of microtasks.
transaction.abort()
is called. In that case all the changes will be rolled back.IDBRequest
ends withabort
event which bubbles toIDBTransaction
Runtime Errors which prevent transaction to be committed (changes won't be saved):
Uncaught exception in the
IDBRequest
success
/error
handler.I/O error:
Fail to write on disk;
Quota exceeded;
OS failure;
Hardware failure;
Browser crash / etc;
Fail to write data due to constrains (e.g. unique index / key);
📝 If for some reason, you want to keep transaction active for some period of time, you should keep creating empty IDBRequest
, to ensure next event loop ticks have pending IDBRequest
:
const txn = db.transaction(["name-store"], "readwrite");
LOG`Try to click other commands, like fillDb! This transaction keeps readwrite lock`;
const nameStore: IDBObjectStore = txn.objectStore("name-store");
let endTransaction = false;
const startTime = Date.now();
setTimeout(() => (endTransaction = true), 5000);
let idbRequestCycles = 0;
function cycleIdbRequest() {
if (endTransaction) {
txn.commit();
return;
}
idbRequestCycles++;
// request non-existing item
const idbRequest = nameStore.get(Infinity);
idbRequest.onsuccess = cycleIdbRequest;
}
cycleIdbRequest();
txn.oncomplete = (e) => {
LOG`Transaction is completed after ${
(Date.now() - startTime) / 1000
} sec. In total ${idbRequestCycles} IDBRequests were created`;
};
Such a code creates lots of empty, chained IDBRequest
queries and keeps transaction alive, while the browser is not entirely blocked (however, it consumes a lot of CPU). Example of output:
📝: It "might" look sometimes a good idea to "keep" transaction alive in that way, when something is calculated in a background, but keep in mind:
It consumes a lot of CPU
Your transaction will be executed slower, if you keep lots of IDBRequests inside the same IDBTransaction (in other words, for long-running IDB transactions, you can see performance degradation).
📝 If you have transaction alive, and keep spawning micro tasks with no any pending IDBRequest
, the transaction will remain alive (however the browser's thread will be blocked too):
const txn = db.transaction(["name-store"], "readwrite");
const startTime = Date.now();
function cyclePromiseLoop(): Promise<void> {
if (Date.now() - startTime > 2000) {
txn.objectStore("name-store").count().onsuccess = (event) => {
const result = event.target.result;
LOG`IDBRequest ended after ${
(Date.now() - startTime) / 1000
} sec. Result: ${result}`;
};
txn.commit();
return Promise.resolve();
}
return Promise.resolve().then(cyclePromiseLoop);
}
await cyclePromiseLoop();
txn.oncomplete = (e) => {
LOG`Transaction is completed after ${(Date.now() - startTime) / 1000} sec.`;
};
IDBRequest
will be executed successfully:
📝 If the next macrotask starts with no pending IDBRequest
or idbRequest.onsuccess
handlers, the transaction will be completed.
Simple way to verify is to put IDBRequest
inside setTimeout
(macrotask). You can see that the transaction will be closed before you start IDBRequest
:
const txn = db.transaction(["name-store"], "readonly");
setTimeout(() => {
const nameStore = txn.objectStore("name-store");
try {
const read = nameStore.count();
read.onerror = (e) => {
LOG`Error performing request: ${e}`;
};
read.onsuccess = () => {
LOG`result: ${read.result}`;
};
} catch (e) {
// This code will be executed (as transaction is not active)
LOG`Runtime Error performing request: ${e}`;
}
}, 0);
txn.oncomplete = (e) => {
LOG`Transaction is completed`;
};
txn.onabort = (e) => {
LOG`idb transaction is aborted`;
};
That code results in constant error, because the transaction is not active:
Transaction durability:
📝 There are 2 types of transaction durability:
strict
: transaction will be consideredcommited
if and only if the data is persisted on diskrelaxed
: browser marks transaction as soon as changes get written to the operating system. "Physically" the data might be written later.
In manual tests the difference between strict
and relaxed
durability might be huge (up to tens milliseconds per transaction). Run "Relaxed vs strict durability" test to see example:
function txn(durability: "strict" | "relaxed"): Promise<void> {
return new Promise((resolve) => {
const txn = idb.transaction(["name-store"], "readwrite", {
durability,
});
const nameStore = txn.objectStore("name-store");
nameStore.add({
name: `Name #${Math.trunc(Math.random() * 1000)}`,
});
txn.oncomplete = () => {
resolve();
};
});
}
LOG`Strict durability, 100 runs`;
let startTime = performance.now();
await Promise.all(Array.from({ length: 100 }).map(() => txn("strict")));
LOG`Strict: ${performance.now() - startTime} ms`;
LOG`Relaxed durability, 100 runs`;
startTime = performance.now();
await Promise.all(Array.from({ length: 100 }).map(() => txn("relaxed")));
LOG`Relaxed: ${performance.now() - startTime} ms`;
For 100 runs the difference would be huge:
Strict durability, 100 runs
Strict: 2278 ms // ~ 22ms per transaction
Relaxed durability, 100 runs
Relaxed: 39 ms // ~0.39ms per transaction
📝 Transaction durability can be defined when you request a transaction:
const txn = db.transaction(["name-store"], "readwrite", {
durability: "relaxed", // or "strict"
});
📝 Firefox and Chrome process transactions differently. Firefox uses relaxed
durability by default, while Chrome is using strict
. Generally, relaxed
durability is reliable enough (unless OS / hardware issues).
How to: IDBObjectStore
The entity that is "analog" for tables in Databases is ObjectStore.
ObjectStore contains all the methods and incapsulates all the processing for the data.
📝 ObjectStore is accessed from the transaction that requested the lock for that particular ObjectStore:
const txn = db.transaction(["name-store"], "readwrite");
const nameStore: IDBObjectStore = txn.objectStore("name-store");
You cannot request an access to ObjectStore which you don't declare in the transaction:
const txn = db.transaction(["name-store"], "readwrite");
const addressStore = txn.objectStore("address-store"); // ERROR!
Object store consist in:
Objects, which are presented by keys and values. Usually key is a part of value, but sometimes you can use out-of-line, which are not stored in value.
Key: presented by either single field / several fields or out-of-line key
Indexes: an additional way of accessing values in your object store. Technically index references the key, which refers to the value in object store.
IDBObjectStore provides all the API to:
Request data
Write / re-write data
Iterate through data: cursors, indexes. You also can mutate / delete data while iterating.
📝 IDBObjectStore operations are ordered. If you subsequentely call methods, they are guaranteed to be executed in the order they were planned:
const txn = idb.transaction(["name-store"], "readwrite");
const nameStore = txn.objectStore("name-store");
// We call method sequentially,
// but IDBRequest are planned without
// awaiting the end of previous one:
nameStore.clear();
nameStore.add({
name: `Mark`,
lastName: `Smith`,
});
nameStore.count().onsuccess = (e) => {
// You can find that TS typing is not the best for IndexedDB
// @ts-expect-error
LOG`1st Count is: ${e.target.result}`;
};
nameStore.put({ name: "Alice" });
nameStore.count().onsuccess = (e) => {
// @ts-expect-error
LOG`2st Count is: ${e.target.result}`;
};
nameStore.clear();
nameStore.count().onsuccess = (e) => {
// @ts-expect-error
LOG`3st Count is: ${e.target.result}`;
};
// Output:
// 1st Count is: 1
// 2st Count is: 2
// 3st Count is: 0
IDBObjectStore methods from readonly / readwrite transactions:
.add(VALUE, ?KEY)
stores the provided VALUE in the ObjectStore. Returns anIDBRequest
. Works only withinreadwrite
transaction- 📝 Browser is using structured clone (deep clone) to make a copy of the value to persist.
.put(item, ?key)
: updates an object by provided key, or adds a new object if there is no object in the provided key. Similar to.add
but able to update the data stored, not only to add items. Works inreadwrite
transaction;.clear()
: removes all the data from the ObjectStore. Returns anIDBRequest
. Works only insidereadwrite
transaction;.count(key | KeyRange)
: returns theIDBRequest
, which would store in result the number of elements, matched to provided key value or KeyRange. If no arguments is provided, it will returns the overall amount of items in ObjectStore;.delete(key | KeyRange)
: removes the data matched tokey
or withinKeyRange
. Only forreadwrite
transaction;.get(key)
: fetches the data matched providedkey
. Returns anIDBRequest
, which, once completed will have the data in.result
field;.getAll(?key | KeyRange, ?count)
: returns theIDBRequest
, which would store in.result
the array of objects, matched to provided key value or KeyRange. If no arguments is provided, it will returns all items in the ObjectStore.count
is used to limit amount of data returned (Like LIMIT in SQL);.getAllKeys(?Key | KeyRange, ?count)
: similar to.getAll
but returns keys, instead of data;.getKey(key | KeyRange)
: similar to.get
but returns key, instead of data;.index(name)
: opens an index (if exists, otherwise throws an error). returnsIDBIndex
which allows to request data from index.
IDBObjectStore in version change transaction:
When we upgrade IDBDatabase version, we have access to ObjectStores and hence we have 2 additional methods:
.createIndex
: creates an index.deleteIndex
: removes an index
IDBObjectStore methods which work with IDBCursor:
Sometimes it's better to open a cursor and iterate through the data instead of fetching all data at once / trying to fetch data first and then update it in separate IDBRequest
.
.openCursor(key | KeyRange, direction)
: returns anIDBRequest
which hasIDBCursorWithValue
in result. the cursor can iterate through data and manipulate it. The level of access fromIDBCursor
depends on the transaction type (readonly / readwrite). Possible directions:next
: increasing order of keys. Iterates through all records;nextunique
: increasing order of keys. Ignores duplicates;prev
: decreasing order. Goes through all records;prevunique
: decreasing order, ignores duplicates;
.openKeyCursor(key | KeyRange, direction)
: same but iterates only for keys;
How to: IDBCursor and IDBCursorWithValue
📝 Cursors allows you to iterate through data record by record, allowing you to effectively mutate the data without multiple separated IDB requests.
📝 Cursor can be one of 2 types: IDBCursor
or IDBCursorWithValue
. .openCursor
method always returns IDBCursorWithValue
. IDBCursorWithValue
has .value
field which keeps the value at the cursor position.
Methods:
.continue()
: moves cursor to the next position;.advance(N)
: similar tocontinue
, but moves the cursor forN
positions;.continuePrimaryKey(key, primaryKey)
: sets the cursor to the given index key and primary key given as arguments;.delete()
: deletes the entry at the cursor position. ReturnsIDBRequest
.update(value)
: updates the value at the cursor position. ReturnsIDBRequest
const txn = idb.transaction(["name-store"], "readwrite");
const nameStore = txn.objectStore("name-store");
let mutatedEntries = 0;
let deletedEntries = 0;
let total = 0;
// Iterate through all the entries
const request = nameStore.openCursor();
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
total++;
if (cursor.value.name.startsWith("N")) {
deletedEntries++;
cursor.delete();
} else {
if (!cursor.value.updated) {
const newValue = { ...cursor.value, updated: true };
mutatedEntries++;
cursor.update(newValue);
}
}
cursor.continue();
}
};
txn.oncomplete = () => {
LOG`Transaction is completed. Deleted: ${deletedEntries}, mutated: ${mutatedEntries}, total: ${total}`;
};
You don't have an access to update
/ delete
methods if you are in readonly
transaction, however, you still have an access to opening cursors and iterating through data.
How to: IDBKeyRange
It's common case, when you need to fetch only part of the data. In that case, iterating through all items and filtering them is not the fastest solution.
It's better to use KeyRanges. KeyRange allows you to define the search interval for your keys or indexes. MDN already has an excellent table with the example:
Range | Code |
Keys >= x | IDBKeyRange.lowerBound(x) |
Keys > x | IDBKeyRange.lowerBound(x, true) |
Keys <= x | IDBKeyRange.upperBound(x) |
Keys < x | IDBKeyRange.upperBound(x, true) |
y>= Keys >= x | IDBKeyRange.bound(x, y) |
y> Keys > x | IDBKeyRange.bound(x, y, true, true) |
y> Keys > x | IDBKeyRange.bound(x, y, true, true) |
y>= Keys > x | IDBKeyRange.bound(x, y, true, false) |
y> Keys >= x | IDBKeyRange.bound(x, y, false, true) |
only x | just use x as a key or IDBKeyRange.only(z) |
How to: IDBIndex
📝 Provides access to index
inside ObjectStore. IDBIndex
has all IDBObjectStore
methods to read the data with the same API, including IDBCursor(WithValue)
requests: count()
, get()
, getAll()
, getAllKeys()
, getKey()
, openCursor()
, openKeyCursor()
.
End-of-article tips!
📝 IndexedDB database is unique by domain. Meaning if you have several sub-domains, you will have separate DBs per domain;
📝 Transaction commit
/abort
methods don't work for upgrade transaction;
📝 DB connection is not guaranteed to be maintained the whole page lifetime. The DB connection can be lost. You should be subscribed to .onclose
event and re-store connection either lazily or proactively as soon as you loose connection
📝 Sometimes you can receive (in sentry or user reports) issues, that Database in not accessible. It's known problem and for users it can be solved only by re-loading the browser (quit and reopen).
📝 As IndexedDB makes structured clone every time you persist / read the data
Subscribe to my newsletter
Read articles from Nik directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nik
Nik
Software engineer @Meta, WhatsApp, building secure, e2ee messaging experience for users. ex-HH.ru ex-Program committee member HolyJS conference