Web Storage API provides powerful local storage capabilities for browsers. This article covers various storage solutions and best practices.

Storage API Basics

localStorage

// Store data
localStorage.setItem('username', 'John');
localStorage.setItem('preferences', JSON.stringify({
  theme: 'dark',
  language: 'en'
}));

// Read data
const username = localStorage.getItem('username');
const preferences = JSON.parse(localStorage.getItem('preferences'));

// Delete data
localStorage.removeItem('username');

// Clear all data
localStorage.clear();

// Get number of storage items
console.log('Storage items:', localStorage.length);

// Iterate all storage items
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(`${key}: ${value}`);
}

sessionStorage

// sessionStorage usage is same as localStorage
// Difference: sessionStorage clears when page session ends

// Store session data
sessionStorage.setItem('sessionId', 'abc123');
sessionStorage.setItem('formData', JSON.stringify({
  step: 1,
  data: {}
}));

// Read session data
const sessionId = sessionStorage.getItem('sessionId');

// Auto-clears when page/tab closes

Storage Events

// Listen for storage changes (cross-tab communication)
window.addEventListener('storage', (event) => {
  console.log('Storage changed:', {
    key: event.key,           // Changed key
    oldValue: event.oldValue, // Old value
    newValue: event.newValue, // New value
    url: event.url,           // Page that triggered change
    storageArea: event.storageArea // localStorage or sessionStorage
  });
});

// Note: Only fires in other tabs, not current page

Wrapped Storage Class

Basic Storage Wrapper

class Storage {
  constructor(type = 'local') {
    this.storage = type === 'local' ? localStorage : sessionStorage;
  }

  // Set value (auto-serialize)
  set(key, value, expires = null) {
    const data = {
      value,
      timestamp: Date.now(),
      expires: expires ? Date.now() + expires : null
    };
    this.storage.setItem(key, JSON.stringify(data));
  }

  // Get value (auto-deserialize)
  get(key, defaultValue = null) {
    const item = this.storage.getItem(key);

    if (!item) return defaultValue;

    try {
      const data = JSON.parse(item);

      // Check expiration
      if (data.expires && Date.now() > data.expires) {
        this.remove(key);
        return defaultValue;
      }

      return data.value;
    } catch {
      return item; // Compatible with non-JSON data
    }
  }

  // Remove
  remove(key) {
    this.storage.removeItem(key);
  }

  // Clear
  clear() {
    this.storage.clear();
  }

  // Check if key exists
  has(key) {
    return this.storage.getItem(key) !== null;
  }

  // Get all keys
  keys() {
    const keys = [];
    for (let i = 0; i < this.storage.length; i++) {
      keys.push(this.storage.key(i));
    }
    return keys;
  }

  // Get storage size (estimate)
  size() {
    let total = 0;
    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      const value = this.storage.getItem(key);
      total += key.length + value.length;
    }
    return total * 2; // UTF-16 encoding
  }
}

// Usage
const storage = new Storage('local');
storage.set('user', { name: 'John', age: 25 }, 3600000); // 1 hour expiry
const user = storage.get('user');

Namespaced Storage

class NamespacedStorage {
  constructor(namespace, type = 'local') {
    this.namespace = namespace;
    this.storage = type === 'local' ? localStorage : sessionStorage;
  }

  _getKey(key) {
    return `${this.namespace}:${key}`;
  }

  set(key, value) {
    this.storage.setItem(this._getKey(key), JSON.stringify(value));
  }

  get(key, defaultValue = null) {
    const item = this.storage.getItem(this._getKey(key));
    if (!item) return defaultValue;
    try {
      return JSON.parse(item);
    } catch {
      return item;
    }
  }

  remove(key) {
    this.storage.removeItem(this._getKey(key));
  }

  // Clear current namespace
  clear() {
    const prefix = `${this.namespace}:`;
    const keysToRemove = [];

    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      if (key.startsWith(prefix)) {
        keysToRemove.push(key);
      }
    }

    keysToRemove.forEach(key => this.storage.removeItem(key));
  }

  // Get all data in current namespace
  getAll() {
    const prefix = `${this.namespace}:`;
    const result = {};

    for (let i = 0; i < this.storage.length; i++) {
      const fullKey = this.storage.key(i);
      if (fullKey.startsWith(prefix)) {
        const key = fullKey.slice(prefix.length);
        result[key] = this.get(key);
      }
    }

    return result;
  }
}

// Usage
const userStorage = new NamespacedStorage('user');
const cartStorage = new NamespacedStorage('cart');

userStorage.set('profile', { name: 'John' });
cartStorage.set('items', [{ id: 1, qty: 2 }]);

IndexedDB

Basic Operations

// Open database
function openDatabase(name, version = 1) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(name, version);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;

      // Create object store
      if (!db.objectStoreNames.contains('users')) {
        const store = db.createObjectStore('users', {
          keyPath: 'id',
          autoIncrement: true
        });

        // Create indexes
        store.createIndex('email', 'email', { unique: true });
        store.createIndex('name', 'name', { unique: false });
      }
    };
  });
}

// Add data
async function addUser(user) {
  const db = await openDatabase('myApp');

  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);
  });
}

// Get data
async function getUser(id) {
  const db = await openDatabase('myApp');

  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);
  });
}

// Update data
async function updateUser(user) {
  const db = await openDatabase('myApp');

  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);
  });
}

// Delete data
async function deleteUser(id) {
  const db = await openDatabase('myApp');

  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);
  });
}

IndexedDB Helper Class

class IndexedDBHelper {
  constructor(dbName, version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }

  async open(upgradeCallback) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onerror = () => reject(request.error);

      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (upgradeCallback) {
          upgradeCallback(db, event.oldVersion, event.newVersion);
        }
      };
    });
  }

  async add(storeName, data) {
    return this._transaction(storeName, 'readwrite', store => store.add(data));
  }

  async put(storeName, data) {
    return this._transaction(storeName, 'readwrite', store => store.put(data));
  }

  async get(storeName, key) {
    return this._transaction(storeName, 'readonly', store => store.get(key));
  }

  async getAll(storeName) {
    return this._transaction(storeName, 'readonly', store => store.getAll());
  }

  async delete(storeName, key) {
    return this._transaction(storeName, 'readwrite', store => store.delete(key));
  }

  async clear(storeName) {
    return this._transaction(storeName, 'readwrite', store => store.clear());
  }

  async count(storeName) {
    return this._transaction(storeName, 'readonly', store => store.count());
  }

  // Query using index
  async getByIndex(storeName, indexName, value) {
    return this._transaction(storeName, 'readonly', store => {
      const index = store.index(indexName);
      return index.get(value);
    });
  }

  async getAllByIndex(storeName, indexName, value) {
    return this._transaction(storeName, 'readonly', store => {
      const index = store.index(indexName);
      return index.getAll(value);
    });
  }

  // Range query
  async getByRange(storeName, indexName, lower, upper) {
    return this._transaction(storeName, 'readonly', store => {
      const index = store.index(indexName);
      const range = IDBKeyRange.bound(lower, upper);
      return index.getAll(range);
    });
  }

  async _transaction(storeName, mode, callback) {
    if (!this.db) {
      throw new Error('Database not opened');
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], mode);
      const store = transaction.objectStore(storeName);
      const request = callback(store);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  close() {
    if (this.db) {
      this.db.close();
      this.db = null;
    }
  }
}

// Usage
const db = new IndexedDBHelper('myApp', 1);

await db.open((database, oldVersion) => {
  if (oldVersion < 1) {
    const store = database.createObjectStore('products', { keyPath: 'id' });
    store.createIndex('category', 'category');
    store.createIndex('price', 'price');
  }
});

await db.add('products', { id: 1, name: 'Phone', category: 'Electronics', price: 999 });
const product = await db.get('products', 1);
const electronics = await db.getAllByIndex('products', 'category', 'Electronics');

Practical Applications

User Preferences

class UserPreferences {
  constructor() {
    this.storage = new Storage('local');
    this.key = 'userPreferences';
    this.defaults = {
      theme: 'light',
      language: 'en',
      fontSize: 16,
      notifications: true,
      autoSave: true
    };
  }

  get() {
    return this.storage.get(this.key, this.defaults);
  }

  set(preferences) {
    const current = this.get();
    const updated = { ...current, ...preferences };
    this.storage.set(this.key, updated);
    this.apply(updated);
    return updated;
  }

  reset() {
    this.storage.set(this.key, this.defaults);
    this.apply(this.defaults);
    return this.defaults;
  }

  apply(preferences) {
    // Apply theme
    document.documentElement.dataset.theme = preferences.theme;

    // Apply font size
    document.documentElement.style.fontSize = `${preferences.fontSize}px`;

    // Dispatch custom event
    window.dispatchEvent(new CustomEvent('preferencesChange', {
      detail: preferences
    }));
  }

  init() {
    const preferences = this.get();
    this.apply(preferences);
    return preferences;
  }
}

// Usage
const prefs = new UserPreferences();
prefs.init();

// Update preferences
prefs.set({ theme: 'dark', fontSize: 18 });

Form Auto-Save

class FormAutoSave {
  constructor(form, options = {}) {
    this.form = form;
    this.key = options.key || `form_${form.id || 'default'}`;
    this.interval = options.interval || 5000;
    this.storage = new Storage('session');

    this.init();
  }

  init() {
    // Restore saved data
    this.restore();

    // Periodic save
    this.timer = setInterval(() => this.save(), this.interval);

    // Save on form change
    this.form.addEventListener('input', () => this.save());

    // Clear on submit
    this.form.addEventListener('submit', () => this.clear());

    // Save before page close
    window.addEventListener('beforeunload', () => this.save());
  }

  save() {
    const formData = new FormData(this.form);
    const data = {};

    for (const [key, value] of formData.entries()) {
      data[key] = value;
    }

    this.storage.set(this.key, data);
    console.log('Form auto-saved');
  }

  restore() {
    const data = this.storage.get(this.key);

    if (!data) return;

    for (const [key, value] of Object.entries(data)) {
      const field = this.form.elements[key];
      if (field) {
        if (field.type === 'checkbox') {
          field.checked = value === 'on' || value === true;
        } else if (field.type === 'radio') {
          const radio = this.form.querySelector(`[name="${key}"][value="${value}"]`);
          if (radio) radio.checked = true;
        } else {
          field.value = value;
        }
      }
    }

    console.log('Form restored');
  }

  clear() {
    this.storage.remove(this.key);
    clearInterval(this.timer);
  }

  destroy() {
    clearInterval(this.timer);
  }
}

// Usage
const form = document.querySelector('#myForm');
new FormAutoSave(form, { interval: 3000 });

Offline Data Cache

class OfflineCache {
  constructor(dbName = 'offlineCache') {
    this.db = new IndexedDBHelper(dbName, 1);
    this.pendingSync = [];
  }

  async init() {
    await this.db.open((database) => {
      // Cache storage
      if (!database.objectStoreNames.contains('cache')) {
        database.createObjectStore('cache', { keyPath: 'key' });
      }

      // Pending sync queue
      if (!database.objectStoreNames.contains('syncQueue')) {
        const store = database.createObjectStore('syncQueue', {
          keyPath: 'id',
          autoIncrement: true
        });
        store.createIndex('timestamp', 'timestamp');
      }
    });
  }

  // Cache data
  async cache(key, data, ttl = 3600000) {
    await this.db.put('cache', {
      key,
      data,
      timestamp: Date.now(),
      expires: Date.now() + ttl
    });
  }

  // Get cache
  async get(key) {
    const item = await this.db.get('cache', key);

    if (!item) return null;

    // Check expiration
    if (Date.now() > item.expires) {
      await this.db.delete('cache', key);
      return null;
    }

    return item.data;
  }

  // Get with network fallback
  async getWithFallback(key, fetchFn) {
    // Try cache first
    const cached = await this.get(key);
    if (cached) return cached;

    // Try network request
    if (navigator.onLine) {
      try {
        const data = await fetchFn();
        await this.cache(key, data);
        return data;
      } catch (error) {
        console.error('Network request failed:', error);
      }
    }

    return null;
  }

  // Add to sync queue
  async addToSyncQueue(action) {
    await this.db.add('syncQueue', {
      action,
      timestamp: Date.now()
    });
  }

  // Process sync queue
  async processSyncQueue() {
    if (!navigator.onLine) return;

    const items = await this.db.getAll('syncQueue');

    for (const item of items) {
      try {
        await this.executeAction(item.action);
        await this.db.delete('syncQueue', item.id);
      } catch (error) {
        console.error('Sync failed:', error);
      }
    }
  }

  async executeAction(action) {
    // Execute based on action type
    const { type, url, method, data } = action;

    await fetch(url, {
      method,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }
}

// Usage
const cache = new OfflineCache();
await cache.init();

// Listen for network status
window.addEventListener('online', () => {
  cache.processSyncQueue();
});

// Add to queue when offline
async function saveData(data) {
  if (navigator.onLine) {
    await fetch('/api/save', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  } else {
    await cache.addToSyncQueue({
      type: 'save',
      url: '/api/save',
      method: 'POST',
      data
    });
  }
}

Shopping Cart Storage

class ShoppingCart {
  constructor() {
    this.storage = new Storage('local');
    this.key = 'shoppingCart';
  }

  getItems() {
    return this.storage.get(this.key, []);
  }

  addItem(product, quantity = 1) {
    const items = this.getItems();
    const existingIndex = items.findIndex(item => item.id === product.id);

    if (existingIndex > -1) {
      items[existingIndex].quantity += quantity;
    } else {
      items.push({
        id: product.id,
        name: product.name,
        price: product.price,
        image: product.image,
        quantity
      });
    }

    this.storage.set(this.key, items);
    this.notifyChange();
    return items;
  }

  updateQuantity(productId, quantity) {
    const items = this.getItems();
    const item = items.find(item => item.id === productId);

    if (item) {
      if (quantity <= 0) {
        return this.removeItem(productId);
      }
      item.quantity = quantity;
      this.storage.set(this.key, items);
      this.notifyChange();
    }

    return items;
  }

  removeItem(productId) {
    let items = this.getItems();
    items = items.filter(item => item.id !== productId);
    this.storage.set(this.key, items);
    this.notifyChange();
    return items;
  }

  clear() {
    this.storage.remove(this.key);
    this.notifyChange();
  }

  getTotal() {
    const items = this.getItems();
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  getCount() {
    const items = this.getItems();
    return items.reduce((sum, item) => sum + item.quantity, 0);
  }

  notifyChange() {
    window.dispatchEvent(new CustomEvent('cartChange', {
      detail: {
        items: this.getItems(),
        total: this.getTotal(),
        count: this.getCount()
      }
    }));
  }
}

// Usage
const cart = new ShoppingCart();

cart.addItem({ id: 1, name: 'Phone', price: 999 });
cart.updateQuantity(1, 2);

window.addEventListener('cartChange', (e) => {
  updateCartUI(e.detail);
});

Best Practices Summary

Web Storage Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Storage Selection                                 │
│   ├── localStorage: Persistent data                │
│   ├── sessionStorage: Session data                 │
│   └── IndexedDB: Large/complex data                │
│                                                     │
│   Data Security                                     │
│   ├── Don't store sensitive info (passwords, tokens)│
│   ├── Consider data encryption                     │
│   └── Be aware of storage quota limits             │
│                                                     │
│   Performance                                       │
│   ├── Avoid frequent read/write operations         │
│   ├── Prefer batch operations                      │
│   └── Set reasonable expiration times              │
│                                                     │
│   User Experience                                   │
│   ├── Provide clear data option                    │
│   ├── Handle storage errors gracefully             │
│   └── Sync state across tabs                       │
│                                                     │
└─────────────────────────────────────────────────────┘
Storage TypeCapacityLifetimeUse Cases
localStorage~5MBPermanentUser settings, themes
sessionStorage~5MBSessionForm data, temp state
IndexedDBLargePermanentOffline data, large files
Cookies~4KBConfigurableAuth, server communication

Master Web Storage API to build efficient frontend data storage solutions.