← Back to Blog
Code Snippets July 17, 2026

LocalStorage with TTL, Serialization & Cross-Tab Sync (JavaScript)

💻 CODE SNIPPET — July 17, 2026

LocalStorage with TTL, Serialization & Cross-Tab Sync (JavaScript)

A robust Store class wrapping localStorage with expiration, auto-JSON, and real-time sync across tabs. Zero dependencies.

class Store {
  constructor(prefix = 'app', fallback = {}) {
    this.prefix = prefix;
    this._fallback = fallback;
    this._listeners = new Map();
    // Cross-tab sync
    window.addEventListener('storage', e => {
      if (e.key?.startsWith(this.prefix)) {
        window.dispatchEvent(new CustomEvent('store-change', {
          detail: { key: e.key, value: e.newValue }
        }));
      }
    });
  }

  set(key, value, ttlMs = null) {
    const entry = JSON.stringify({
      v: value,
      t: Date.now(),
      e: ttlMs ? Date.now() + ttlMs : null
    });
    try { localStorage.setItem(`${this.prefix}:${key}`, entry); }
    catch { Object.assign(this._fallback, { [key]: value }); }
  }

  get(key, defaultValue = undefined) {
    const raw = localStorage.getItem(`${this.prefix}:${key}`)
      ?? JSON.stringify({ v: this._fallback[key] });
    const entry = JSON.parse(raw);
    if (entry.e && Date.now() > entry.e) return defaultValue;
    return entry.v ?? defaultValue;
  }

  on(key, fn) { this._listeners.set(key, fn); }
  clearExpired() {
    Object.keys(localStorage).filter(k => k.startsWith(this.prefix))
      .forEach(k => {
        const e = JSON.parse(localStorage.getItem(k));
        if (e.e && Date.now() > e.e) localStorage.removeItem(k);
      });
  }
}

// Usage:
const store = new Store('myapp');
store.set('theme', 'dark', 86400000);  // 24hr TTL
const theme = store.get('theme', 'light');  // default: light


Pro Tip: The storage event only fires on other tabs. The CustomEvent bridge handles same-page listeners. Safari private browsing falls back to in-memory storage gracefully.