Advanced Web Storage
Storage Options Overview
Modern browsers offer multiple storage APIs:
| Storage Type | Capacity | Persistence | Use Case |
|---|---|---|---|
| localStorage | ~5-10 MB | Forever | User preferences, settings |
| sessionStorage | ~5-10 MB | Tab session | Temporary form data |
| IndexedDB | 50+ MB | Forever | Large datasets, offline apps |
| Cookies | 4 KB | Configurable | Authentication tokens |
sessionStorage Advanced Patterns
Multi-Step Form State
Preserve form data across page navigations within same tab:
// Save form state on every change
function saveFormState() {
const formData = {
step: currentStep,
personalInfo: {
name: document.getElementById('name').value,
email: document.getElementById('email').value
},
address: {
street: document.getElementById('street').value,
city: document.getElementById('city').value
}
};
sessionStorage.setItem('multiStepForm', JSON.stringify(formData));
}
// Restore form state on page load
function restoreFormState() {
const saved = sessionStorage.getItem('multiStepForm');
if (saved) {
const formData = JSON.parse(saved);
currentStep = formData.step;
document.getElementById('name').value = formData.personalInfo.name;
document.getElementById('email').value = formData.personalInfo.email;
document.getElementById('street').value = formData.address.street;
document.getElementById('city').value = formData.address.city;
}
}
// Clear on successful submission
function handleSubmit() {
// ... submit logic
sessionStorage.removeItem('multiStepForm');
}
Shopping Cart Session
Keep cart data only for current browsing session:
class SessionCart {
constructor() {
this.storageKey = 'shoppingCart';
}
getCart() {
const cart = sessionStorage.getItem(this.storageKey);
return cart ? JSON.parse(cart) : [];
}
addItem(product) {
const cart = this.getCart();
const existing = cart.find(item => item.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
cart.push({ ...product, quantity: 1 });
}
sessionStorage.setItem(this.storageKey, JSON.stringify(cart));
this.updateCartUI();
}
removeItem(productId) {
const cart = this.getCart();
const filtered = cart.filter(item => item.id !== productId);
sessionStorage.setItem(this.storageKey, JSON.stringify(filtered));
this.updateCartUI();
}
getTotal() {
const cart = this.getCart();
return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
clear() {
sessionStorage.removeItem(this.storageKey);
this.updateCartUI();
}
updateCartUI() {
const count = this.getCart().length;
document.getElementById('cart-count').textContent = count;
}
}
// Usage
const cart = new SessionCart();
cart.addItem({ id: 1, name: 'Laptop', price: 999 });
console.log(cart.getTotal()); // 999
Tab-Specific State
Track state unique to each browser tab:
// Generate unique tab ID
function getTabId() {
let tabId = sessionStorage.getItem('tabId');
if (!tabId) {
tabId = 'tab_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('tabId', tabId);
}
return tabId;
}
// Track active editor per tab
class TabEditor {
constructor() {
this.tabId = getTabId();
this.storageKey = `editor_${this.tabId}`;
}
saveContent(content) {
sessionStorage.setItem(this.storageKey, content);
}
loadContent() {
return sessionStorage.getItem(this.storageKey) || '';
}
}
// Each tab has its own editor state
const editor = new TabEditor();
editor.saveContent('Tab-specific draft...');
Storage Events
Listen for storage changes across tabs/windows:
Cross-Tab Communication
// Listen for changes from other tabs
window.addEventListener('storage', (event) => {
if (event.key === 'sharedMessage') {
console.log('Message from another tab:', event.newValue);
displayMessage(event.newValue);
}
});
// Send message to other tabs
function broadcastToOtherTabs(message) {
localStorage.setItem('sharedMessage', JSON.stringify({
message,
timestamp: Date.now()
}));
}
// Usage
broadcastToOtherTabs('User logged out');
Sync User Preferences Across Tabs
// Tab 1: Change theme
function setTheme(theme) {
localStorage.setItem('theme', theme);
applyTheme(theme);
}
// Tab 2: Listen and update
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
applyTheme(event.newValue);
}
});
function applyTheme(theme) {
document.body.className = theme;
document.getElementById('theme-select').value = theme;
}
Real-Time Sync Warning
// Warn user if data changed in another tab
let lastKnownData = localStorage.getItem('userData');
window.addEventListener('storage', (event) => {
if (event.key === 'userData' && event.newValue !== lastKnownData) {
const reload = confirm('Data was updated in another tab. Reload page?');
if (reload) {
location.reload();
} else {
lastKnownData = event.newValue;
}
}
});
IndexedDB Basics
IndexedDB is a low-level API for storing large amounts of structured data:
Opening a Database
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
// Create object stores (like tables)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create "users" store
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
}
// Create "posts" store
if (!db.objectStoreNames.contains('posts')) {
const postStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true });
postStore.createIndex('userId', 'userId', { unique: false });
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject('Database error: ' + event.target.error);
};
});
}
CRUD Operations
class UserDB {
constructor() {
this.dbPromise = openDatabase();
}
async add(user) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(id) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll() {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async update(user) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(id) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async findByEmail(email) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Usage
const userDB = new UserDB();
// Add user
const userId = await userDB.add({
name: 'Alice',
email: 'alice@example.com',
age: 25
});
// Get user
const user = await userDB.get(userId);
console.log(user);
// Update user
await userDB.update({
id: userId,
name: 'Alice Smith',
email: 'alice@example.com',
age: 26
});
// Find by email
const found = await userDB.findByEmail('alice@example.com');
// Get all users
const allUsers = await userDB.getAll();
// Delete user
await userDB.delete(userId);
Querying with Cursors
async function getUsersOlderThan(minAge) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.age > minAge) {
results.push(cursor.value);
}
cursor.continue();
} else {
// No more results
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
// Usage
const adults = await getUsersOlderThan(18);
Practical Example: Offline Todo App
class OfflineTodoApp {
constructor() {
this.dbPromise = this.initDB();
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('TodoApp', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
store.createIndex('completed', 'completed', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async addTodo(text) {
const db = await this.dbPromise;
const todo = {
text,
completed: false,
createdAt: new Date().toISOString()
};
return new Promise((resolve, reject) => {
const transaction = db.transaction(['todos'], 'readwrite');
const store = transaction.objectStore('todos');
const request = store.add(todo);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getTodos(filter = 'all') {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['todos'], 'readonly');
const store = transaction.objectStore('todos');
const request = store.getAll();
request.onsuccess = () => {
let todos = request.result;
if (filter === 'active') {
todos = todos.filter(t => !t.completed);
} else if (filter === 'completed') {
todos = todos.filter(t => t.completed);
}
resolve(todos);
};
request.onerror = () => reject(request.error);
});
}
async toggleTodo(id) {
const db = await this.dbPromise;
return new Promise(async (resolve, reject) => {
const transaction = db.transaction(['todos'], 'readwrite');
const store = transaction.objectStore('todos');
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const todo = getRequest.result;
todo.completed = !todo.completed;
const updateRequest = store.put(todo);
updateRequest.onsuccess = () => resolve();
updateRequest.onerror = () => reject(updateRequest.error);
};
});
}
async deleteTodo(id) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction(['todos'], 'readwrite');
const store = transaction.objectStore('todos');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Usage
const app = new OfflineTodoApp();
await app.addTodo('Learn IndexedDB');
const todos = await app.getTodos();
console.log(todos);
Storage Quota Management
Check and request more storage:
// Check current usage
async function checkStorageUsage() {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
const percentUsed = (estimate.usage / estimate.quota) * 100;
console.log(`Using ${estimate.usage} of ${estimate.quota} bytes (${percentUsed.toFixed(2)}%)`);
return {
used: estimate.usage,
total: estimate.quota,
available: estimate.quota - estimate.usage,
percentUsed
};
}
}
// Request persistent storage
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
console.log(`Persistent storage: ${isPersisted ? 'granted' : 'denied'}`);
return isPersisted;
}
}
// Check if storage is persistent
async function isStoragePersistent() {
if (navigator.storage && navigator.storage.persisted) {
return await navigator.storage.persisted();
}
return false;
}
Choosing the Right Storage
function getStorageRecommendation(dataSize, needsPersistence, needsStructure) {
// Small data (< 5MB)
if (dataSize < 5 * 1024 * 1024) {
if (!needsPersistence) {
return 'sessionStorage'; // Temporary data
}
if (!needsStructure) {
return 'localStorage'; // Simple key-value
}
}
// Large or structured data
if (needsStructure || dataSize >= 5 * 1024 * 1024) {
return 'IndexedDB'; // Complex queries, large datasets
}
return 'localStorage'; // Default for persistent simple data
}
// Examples
console.log(getStorageRecommendation(1024, false, false)); // sessionStorage
console.log(getStorageRecommendation(1024, true, false)); // localStorage
console.log(getStorageRecommendation(10 * 1024 * 1024, true, true)); // IndexedDB
Best Practices
✅ DO:
// Handle errors
try {
localStorage.setItem('key', 'value');
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
}
}
// Use sessionStorage for temporary data
sessionStorage.setItem('formDraft', JSON.stringify(formData));
// Use IndexedDB for large datasets
const db = new UserDB();
await db.add(largeUserObject);
// Clean up when done
window.addEventListener('beforeunload', () => {
sessionStorage.removeItem('tempData');
});
❌ DON’T:
// Don't store sensitive data unencrypted
localStorage.setItem('password', userPassword); // ❌ Never!
localStorage.setItem('creditCard', cardNumber); // ❌ Never!
// Don't store huge objects in localStorage
localStorage.setItem('data', JSON.stringify(megabyteObject)); // ❌ Use IndexedDB
// Don't forget to parse JSON
const user = localStorage.getItem('user');
console.log(user.name); // ❌ user is a string!
const user = JSON.parse(localStorage.getItem('user')); // ✅
// Don't use synchronous IndexedDB methods (deprecated)
// Use promises/async-await instead
Summary
Storage Comparison:
| Feature | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|
| Capacity | ~5-10 MB | ~5-10 MB | 50+ MB |
| Persistence | Forever | Tab session | Forever |
| Async | No | No | Yes |
| Structure | Key-value | Key-value | Object stores |
| Queries | No | No | Yes (indexes) |
| Use Case | Preferences | Form drafts | Offline apps |
Quick Decision:
- User preferences → localStorage
- Form drafts → sessionStorage
- Large datasets → IndexedDB
- Cross-tab sync → localStorage + storage events
Next Article: Geolocation API