javascript-today

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