29
loading...
This website collects cookies to deliver better user experience
It can typically store 1GB of data which makes it suitable for large objects, files, images etc. Moving these items out of memory can make an application faster and more efficient.
Unlike cookies and Web Storage (localStorage
and sessionStorage
), IndexedDB stores native JavaScript object data. There's no need to serialize into JSON strings or deserialize back again.
IndexedDB access is asynchronous so it has minimal impact on the main JavaScript processing thread.
Note that Web Storage is synchronous: your JavaScript code pauses execution while it accesses data. This can cause performance issues when saving larger datasets.
The IndexedDB API uses older callback and event methods so a Promise-based wrapper library is practical.
async
class constructors and Proxy get/set handlers are not possible in JavaScript. This poses some challenges for state management systems.
State
management system in 35 lines of JavaScript. It offers the following features:You can define a state with a name
(string) and a value
(primitive, array, object, etc). An IndexedDB object store saves these values using the name as an index.
Any JavaScript component can set
or get
a value by its name.
When a value is set
, the state manager alerts all subscribed components about the change. A component subscribes itself via the State
constructor, or by setting or getting a named value.
State
manager. It defines two Web Components which both access the same todolist
array of tasks managed by State
objects:todo-list.js
: renders the todolist
HTML and removes an item when the user clicks it's 'done' button.todo-add.js
: shows an "add new item" form which appends new tasks to the todolist
array.Note: A single todo list component would be more practical, but this demonstrates how two isolated classes can share the same state.
name
.js/lib/indexeddb.js
script defines an IndexedDB
class with a constructor. It accepts a database name, version, and upgrade function. It returns the instantiated object following a successful connection to the IndexedDB database:// IndexedDB wrapper class
export class IndexedDB {
// connect to IndexedDB database
constructor(dbName, dbVersion, dbUpgrade) {
return new Promise((resolve, reject) => {
// connection object
this.db = null;
// no support
if (!('indexedDB' in window)) reject('not supported');
// open database
const dbOpen = indexedDB.open(dbName, dbVersion);
if (dbUpgrade) {
// database upgrade event
dbOpen.onupgradeneeded = e => {
dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion);
};
}
dbOpen.onsuccess = () => {
this.db = dbOpen.result;
resolve( this );
};
dbOpen.onerror = e => {
reject(`IndexedDB error: ${ e.target.errorCode }`);
};
});
}
set
method stores a value
with the name
identifier in the storeName
object store. IndexedDB handles all operations in a transaction which triggers events that resolve or reject the Promise:// store item
set(storeName, name, value) {
return new Promise((resolve, reject) => {
// new transaction
const
transaction = this.db.transaction(storeName, 'readwrite'),
store = transaction.objectStore(storeName);
// write record
store.put(value, name);
transaction.oncomplete = () => {
resolve(true); // success
};
transaction.onerror = () => {
reject(transaction.error); // failure
};
});
}
get
method retrieves the value
with the name
identifier in the storeName
object store:// get named item
get(storeName, name) {
return new Promise((resolve, reject) => {
// new transaction
const
transaction = this.db.transaction(storeName, 'readonly'),
store = transaction.objectStore(storeName),
// read record
request = store.get(name);
request.onsuccess = () => {
resolve(request.result); // success
};
request.onerror = () => {
reject(request.error); // failure
};
});
}
}
js/lib/state.js
script imports IndexedDB
and defines a State
class. It shares five static
property values across all instantiations:dbName
: the name of the IndexedDB database used for state storage ("stateDB"
)dbVersion
: the database version number (1
)storeName
: the name of the object store used to store all name/value pairs ("state"
)DB
: a reference to a single IndexedDB
object used to access the database, andtarget
: an EventTarget() object which can dispatch and receive events across all State
objects.
// simple state handler
import { IndexedDB } from './indexeddb.js';
export class State {
static dbName = 'stateDB';
static dbVersion = 1;
static storeName = 'state';
static DB = null;
static target = new EventTarget();
observed
names, andupdateCallback
function. This function receives the name
and value
whenever a state updates.set
events called when state changes. It runs the updateCallback
function when the passed name
is being observed
.// object constructor
constructor(observed, updateCallback) {
// state change callback
this.updateCallback = updateCallback;
// observed properties
this.observed = new Set(observed);
// subscribe to set events
State.target.addEventListener('set', e => {
if (this.updateCallback && this.observed.has( e.detail.name )) {
this.updateCallback(e.detail.name, e.detail.value);
}
});
}
dbConnect
method establishes a connection and reuses it across all State
objects. On the first run, it creates a new object store named state
(as defined in the static storeName
property):// connect to IndexedDB database
async dbConnect() {
State.DB = State.DB || await new IndexedDB(
State.dbName,
State.dbVersion,
(db, oldVersion, newVersion) => {
// upgrade database
switch (oldVersion) {
case 0: {
db.createObjectStore( State.storeName );
}
}
});
return State.DB;
}
set
method updates a named value. It adds the name
to the observed
list, connects to the IndexedDB database, sets the new value, and triggers a set
CustomEvent which all State
objects receive:// set value in DB
async set(name, value) {
// add observed property
this.observed.add(name);
// database update
const db = await this.dbConnect();
await db.set( State.storeName, name, value );
// raise event
const event = new CustomEvent('set', { detail: { name, value } });
State.target.dispatchEvent(event);
}
get
method returns a named value. It adds the name
to the observed
list, connects to the IndexedDB database, and retrieves the indexed data:// get value from DB
async get(name) {
// add observed property
this.observed.add(name);
// database fetch
const db = await this.dbConnect();
return await db.get( State.storeName, name );
}
}
State
object, e.g.import { State } from './state.js';
(async () => {
// instantiate
const state = new State([], stateUpdated);
// get latest value and default to zero
let myval = await state.get('myval') || 0;
// set a new state value
await state.set('myval', myval + 1);
// callback runs when myval updates
function stateUpdated(name, value) {
console.log(`${ name } is now ${ value }`)
}
})()
new State(['myval'], (name, value) => {
console.log(`I also see ${ name } is now set to ${ value }!`)
});
index.html
file defines two custom elements:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IndexedDB state management to-do list</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="./css/main.css" />
<script type="module" src="./js/main.js"></script>
</head>
<body>
<h1>IndexedDB state management to-do list</h1>
<todo-list></todo-list>
<todo-add></todo-add>
</body>
</html>
<todo-list>
- the todo list controlled by ./js/components/todo-list.js
which updates the list when tasks are added and removed, and<todo-add>
- a form to add items to the todo list controlled by ./js/components/todo-list.js
../js/main.js
loads both component modules:// load components
import './components/todo-add.js';
import './components/todo-list.js';
todolist
state. Web Components are beyond the scope of this article, but the basics are:<todo-list>
). The name must contain a dash (-
) to avoid clashes with current or future HTML elements.extends
HTMLElement defines the functionality. The constructor must call super()
.connectedCallback()
method when it's ready to update the DOM. The method can append content, optionally using an encapsulated Shadow DOM which is not accessible to other scripts../js/components/todo-list.js
defines the TodoList
class for the <todo-list>
component. It shows a list of tasks and handles deletion when the user clicks a 'done' button. The class sets static HTML strings and creates a new State
object. This monitors the a todolist
variable and runs the object's render()
method when its value changes:
`js
<style>
ol { padding: 0; margin: 1em 0; }
li { list-style: numeric inside; padding: 0.5em; margin: 0; }
li:hover, li:focus-within { background-color: #eee; }
button { width: 4em; float: right; }
</style>
;<li>$1 <button type="button" value="$2">done</button></li>
;
render()
method receives the updated name
and value
(only todolist
will arrive). It stores the list as a local object property then appends HTML to the Shadow DOM (created by the connectedCallback()
method):
`js// update state
this[name] = value;
// create new list
let list = '';
this.todolist.map((v, i) => {
list += TodoList.template.replace('$1', v).replace('$2', i);
});
this.shadow.innerHTML = `${ TodoList.style }<ol>${ list }</ol>`;
connectedCallback()
method runs when the DOM is ready. It:todolist
state to the render()
method, andtodolist
state. The render()
method will automatically execute because the state changed.
`jsthis.shadow = this.attachShadow({ mode: 'closed' });
this.render('todolist', await this.state.get('todolist') || []);
// remove item event
this.shadow.addEventListener('click', async e => {
if (e.target.nodeName !== 'BUTTON') return;
this.todolist.splice(e.target.value, 1);
await this.state.set('todolist', this.todolist);
});
TodoList
class is then registered for the <todo-list>
component:
`js
./js/components/todo-add.js
defines the TodoAdd
class for the <todo-add>
component. It shows a form that can append new tasks to the todolist
state. It sets a static HTML string and creates a new State
object. This monitors the todolist
state and retains it as a local object property:
`js
<style>
form { display: flex; justify-content: space-between; padding: 0.5em; }
input { flex: 3 1 10em; font-size: 1em; padding: 6px; }
button { width: 4em; }
</style>
<form method="post">
<input type="text" name="add" placeholder="add new item" required />
<button>add</button>
</form>
;
connectedCallback()
method runs when the DOM is ready. It:todolist
state into a local property which defaults to an empty arraytodolist
state (which, in turn, updates the <todo-list>
component). It then clears the input field so you can add another task.
`js// get latest todo list
this.todolist = await this.state.get('todolist') || [];
const shadow = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = TodoAdd.template;
const add = shadow.querySelector('input');
shadow.querySelector('form').addEventListener('submit', async e => {
e.preventDefault();
// add item to list
await this.state.set('todolist', this.todolist.concat(add.value));
add.value = '';
add.focus();
});
TodoAdd
class is then registered for the <todo-add>
component:
`js