LocalStorage and IndexedDB
Written September 2012- specs and browser support will change! Updated 2024.
Local Storage
- All browsers give 5Mb of key-value storage for local (persistent) + 5Mb for sessionStorage.
- MDN
localStorage["key"] = 29; //or localStorage.setItem(key, value); //always coerce types var find = parseInt(localStorage["key"], 10); //or localStorage.getItem(key) var size = localStorage.length;
- storage is scoped to the origin (https://website/). NB file:// is not allowed.
- sessionStorage is for as long as the browser is open.
- localStorage is persistent across browser restarts.
- It's dead simple but strings-only, synchronous and IO-limited (so can be slow)
See IndexedDB for larger data sets
IndexedDb
- Available in all modern browsers (Safari from 2014), but verify specific features
- MDN
- Almost everything returns asynchronous requests. Always hook on .onerror and .onsuccess (which adds lots of boilerplate).
Open/create the database
- Initialize and upgrade database through .onupgradeneeded (.createObjectStore, .createIndex, .deleteObjectStore)
- ObjectStores can have autoincrement key generators or you explicitly set them.
async function createOrRecreateDatabase() {
// Open a connection to the database
const request = indexedDB.open("CountryDb", 1);
// If the database does not exist - or needs updating
request.onupgradeneeded = event => {
const db = event.target.result;
// If the Countries object store already exists, delete it
if (db.objectStoreNames.contains("Countries")) {
db.deleteObjectStore("Countries");
}
// Create the Countries object store with a key path of 'id' and autoIncrement set to true
const countryStore = db.createObjectStore("Countries", { keyPath: "id", autoIncrement: true });
// Create an index on the 'Name' property
countryStore.createIndex("by_name", "LowerCaseName", { unique: false });
// Add initial data
// for case insensitive search, add a lowercase property to the object
const countries = [
{ Name: "Australia", ShortName: "AU", LowerCaseName: "Australia".toLowerCase() },
{ Name: "Belgium", ShortName: "BE", LowerCaseName: "Belgium".toLowerCase() },
{ Name: "Brazil", ShortName: "BR", LowerCaseName: "Brazil".toLowerCase() },
{ Name: "Canada", ShortName: "CA", LowerCaseName: "Canada".toLowerCase() },
{ Name: "China", ShortName: "CN", LowerCaseName: "China".toLowerCase() },
// Add more countries as needed...
];
countries.forEach(country => {
countryStore.add(country);
});
};
request.onerror = event => {
console.error("Database error: ", event.target.error);
};
request.onsuccess = event => {
console.log("Database created successfully");
};
}
// Open a connection to the database
const request = indexedDB.open("CountryDb", 1);
// If the database does not exist - or needs updating
request.onupgradeneeded = event => {
const db = event.target.result;
// If the Countries object store already exists, delete it
if (db.objectStoreNames.contains("Countries")) {
db.deleteObjectStore("Countries");
}
// Create the Countries object store with a key path of 'id' and autoIncrement set to true
const countryStore = db.createObjectStore("Countries", { keyPath: "id", autoIncrement: true });
// Create an index on the 'Name' property
countryStore.createIndex("by_name", "LowerCaseName", { unique: false });
// Add initial data
// for case insensitive search, add a lowercase property to the object
const countries = [
{ Name: "Australia", ShortName: "AU", LowerCaseName: "Australia".toLowerCase() },
{ Name: "Belgium", ShortName: "BE", LowerCaseName: "Belgium".toLowerCase() },
{ Name: "Brazil", ShortName: "BR", LowerCaseName: "Brazil".toLowerCase() },
{ Name: "Canada", ShortName: "CA", LowerCaseName: "Canada".toLowerCase() },
{ Name: "China", ShortName: "CN", LowerCaseName: "China".toLowerCase() },
// Add more countries as needed...
];
countries.forEach(country => {
countryStore.add(country);
});
};
request.onerror = event => {
console.error("Database error: ", event.target.error);
};
request.onsuccess = event => {
console.log("Database created successfully");
};
}
ObjectStore- saving
- ObjectStore = table. You get at it via database transaction.
- Transactions name the tables and whether readonly (default) or readwrite.
- You can .add (=insert) or .put (=upsert)
async function updateCountryName(id, newName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readwrite");
const store = transaction.objectStore("Countries");
const getRequest = store.get(id);
getRequest.onerror = event => {
console.error("Error fetching country: ", event.target.error);
reject("Failed to fetch country");
};
getRequest.onsuccess = event => {
const data = event.target.result;
data.Name = newName; // Update the name
data.LowerCaseName = newName.toLowerCase();
const putRequest = store.put(data);
putRequest.onerror = event => {
console.error("Error updating country: ", event.target.error);
reject("Failed to update country");
};
putRequest.onsuccess = () => {
resolve("Country updated successfully");
};
};
};
});
}
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readwrite");
const store = transaction.objectStore("Countries");
const getRequest = store.get(id);
getRequest.onerror = event => {
console.error("Error fetching country: ", event.target.error);
reject("Failed to fetch country");
};
getRequest.onsuccess = event => {
const data = event.target.result;
data.Name = newName; // Update the name
data.LowerCaseName = newName.toLowerCase();
const putRequest = store.put(data);
putRequest.onerror = event => {
console.error("Error updating country: ", event.target.error);
reject("Failed to update country");
};
putRequest.onsuccess = () => {
resolve("Country updated successfully");
};
};
};
});
}
ObjectStore - reading
.get(key)
async function fetchAllCountries() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readonly");
const store = transaction.objectStore("Countries");
const getAllRequest = store.getAll();
getAllRequest.onerror = event => {
console.error("Error fetching countries: ", event.target.error);
reject("Failed to fetch countries");
};
getAllRequest.onsuccess = event => {
resolve(event.target.result);
};
};
});
}
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readonly");
const store = transaction.objectStore("Countries");
const getAllRequest = store.getAll();
getAllRequest.onerror = event => {
console.error("Error fetching countries: ", event.target.error);
reject("Failed to fetch countries");
};
getAllRequest.onsuccess = event => {
resolve(event.target.result);
};
};
});
}
.openCursor(range) often via .index(name)
async function searchCountriesByNamePrefix(namePrefix) {
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readonly");
const store = transaction.objectStore("Countries");
const index = store.index("by_name");
// Convert the prefix to lowercase for case-insensitive search
const lowercasedPrefix = namePrefix.toLowerCase();
const range = IDBKeyRange.bound(lowercasedPrefix, lowercasedPrefix + '\uffff');
const cursorRequest = index.openCursor(range);
const results = [];
let count = 0; // Counter for the number of records
cursorRequest.onerror = event => {
console.error("Error searching countries: ", event.target.error);
reject("Failed to search countries");
};
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (cursor && count < 100) {
if (cursor.value.Name.toLowerCase().startsWith(lowercasedPrefix)) {
results.push(cursor.value);
}
cursor.continue();
} else {
// No more results
resolve(results);
}
};
};
});
}
return new Promise((resolve, reject) => {
const request = indexedDB.open("CountryDb", 1);
request.onerror = event => {
console.error("Database error: ", event.target.error);
reject("Failed to open DB");
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction("Countries", "readonly");
const store = transaction.objectStore("Countries");
const index = store.index("by_name");
// Convert the prefix to lowercase for case-insensitive search
const lowercasedPrefix = namePrefix.toLowerCase();
const range = IDBKeyRange.bound(lowercasedPrefix, lowercasedPrefix + '\uffff');
const cursorRequest = index.openCursor(range);
const results = [];
let count = 0; // Counter for the number of records
cursorRequest.onerror = event => {
console.error("Error searching countries: ", event.target.error);
reject("Failed to search countries");
};
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (cursor && count < 100) {
if (cursor.value.Name.toLowerCase().startsWith(lowercasedPrefix)) {
results.push(cursor.value);
}
cursor.continue();
} else {
// No more results
resolve(results);
}
};
};
});
}
Demo
Using dbOperations.js (all local)
Add a New Country
Search Countries
How much storage?
navigator.storage.estimate().then(function(estimate) { })
Usage
Quota
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
if (bytes === 0) return '0';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}