Home Module 14 Local Storage

1. Introduction

Browser storage APIs let you save data in the user's browser — data that survives page refreshes. localStorage persists until explicitly cleared. sessionStorage persists only until the tab is closed. Both use a simple key-value string API. Combined with JSON, they let you store complex objects.

Common uses: theme preferences, cached API responses, draft content, shopping cart state, authentication tokens, and feature flags.

2. Theory

2.1 localStorage basics

// Set a value — both key and value must be strings
localStorage.setItem('theme', 'dark');
localStorage.setItem('lang',  'en');

// Get a value — returns null if key doesn't exist
const theme = localStorage.getItem('theme');
console.log(theme); // 'dark'

const missing = localStorage.getItem('nonexistent');
console.log(missing); // null — not undefined!

// Remove a single key
localStorage.removeItem('lang');

// Clear ALL keys (careful — removes everything for the origin)
localStorage.clear();

// Number of stored items
console.log(localStorage.length); // 0 after clear

// Iterate all keys
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  console.log(key, localStorage.getItem(key));
}

2.2 Storing objects with JSON

// localStorage only stores strings — use JSON for objects
const user = { name: 'Alice', role: 'admin', age: 28 };

// Save
localStorage.setItem('user', JSON.stringify(user));

// Load
const raw    = localStorage.getItem('user');
const loaded = raw ? JSON.parse(raw) : null;
console.log(loaded?.name); // 'Alice'

// Safe loading with default
function load(key, defaultValue = null) {
  try {
    const raw = localStorage.getItem(key);
    return raw !== null ? JSON.parse(raw) : defaultValue;
  } catch {
    return defaultValue; // JSON.parse can throw on corrupt data
  }
}

function save(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch (err) {
    // QuotaExceededError — storage is full
    console.error('Storage full:', err);
    return false;
  }
}

2.3 sessionStorage — same API, different lifetime

// sessionStorage — cleared when the tab is closed
sessionStorage.setItem('draft', JSON.stringify({ title: 'WIP', body: '...' }));
const draft = JSON.parse(sessionStorage.getItem('draft') ?? 'null');

// Comparison:
// localStorage   — persists forever (until cleared), shared across tabs
// sessionStorage — persists for session (tab lifetime), isolated per tab
// cookies        — sent with HTTP requests, can have expiry, size-limited
// IndexedDB      — large structured data, async, key-value or indexes

2.4 The storage event — sync across tabs

// storage event fires in OTHER tabs when localStorage changes (not the writing tab)
window.addEventListener('storage', event => {
  console.log('Key changed:', event.key);
  console.log('Old value:',  event.oldValue);
  console.log('New value:',  event.newValue);
  console.log('URL:',        event.url); // which tab changed it

  if (event.key === 'theme') {
    applyTheme(event.newValue);
  }
});

// Use case: sync dark/light mode, auth state, cart count across tabs

2.5 Storage capacity and limitations

// localStorage limits:
// - ~5-10 MB per origin (varies by browser)
// - Only strings (use JSON for objects)
// - Synchronous (blocks the main thread — avoid huge reads)
// - Not accessible by service workers directly
// - Not available in private/incognito mode (or throws QuotaExceededError)
// - Shared across all tabs for the same origin (protocol + domain + port)

// Check available space (rough estimate)
function getStorageUsage() {
  let total = 0;
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    total += key.length + (localStorage.getItem(key)?.length || 0);
  }
  return `~${(total / 1024).toFixed(1)} KB`;
}

2.6 Pattern: versioned storage

const STORAGE_VERSION = '2';
const VERSION_KEY     = 'app_version';

function migrateStorage() {
  const storedVersion = localStorage.getItem(VERSION_KEY);
  if (storedVersion !== STORAGE_VERSION) {
    console.log(`Migrating from v${storedVersion} to v${STORAGE_VERSION}`);
    localStorage.clear(); // simple migration: clear old data
    localStorage.setItem(VERSION_KEY, STORAGE_VERSION);
  }
}

migrateStorage(); // call on app start

2.7 Common storage patterns

// Theme persistence
function getTheme() {
  return localStorage.getItem('theme') ?? 'light';
}

function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.body.classList.toggle('dark', theme === 'dark');
}

// Apply on load
setTheme(getTheme());

// Shopping cart
function getCart() {
  return load('cart', []);
}

function saveCart(items) {
  save('cart', items);
}

function addToCart(product) {
  const cart = getCart();
  const idx  = cart.findIndex(i => i.id === product.id);
  if (idx >= 0) {
    cart[idx].qty++;
  } else {
    cart.push({ ...product, qty: 1 });
  }
  saveCart(cart);
  return cart;
}

// Form draft saving
const input = document.querySelector('#post-body');
input.value = localStorage.getItem('draft') ?? '';
input.addEventListener('input', () => {
  localStorage.setItem('draft', input.value);
});
document.querySelector('#submit').addEventListener('click', () => {
  submit(input.value);
  localStorage.removeItem('draft'); // clear draft after submit
});

3. Real World Example

// Preferences manager — theme, language, accessibility
const PREFS_KEY = 'user_prefs';

const defaultPrefs = Object.freeze({
  theme:       'light',
  fontSize:    16,
  language:    'en',
  reducedMotion: false,
  highContrast: false
});

function loadPrefs() {
  const raw = localStorage.getItem(PREFS_KEY);
  if (!raw) return { ...defaultPrefs };
  try {
    return { ...defaultPrefs, ...JSON.parse(raw) };
  } catch {
    return { ...defaultPrefs };
  }
}

function savePrefs(changes) {
  const current = loadPrefs();
  const updated = { ...current, ...changes };
  localStorage.setItem(PREFS_KEY, JSON.stringify(updated));
  return updated;
}

function applyPrefs(prefs) {
  document.body.classList.toggle('dark', prefs.theme === 'dark');
  document.body.classList.toggle('high-contrast', prefs.highContrast);
  document.documentElement.style.fontSize = `${prefs.fontSize}px`;
  document.documentElement.setAttribute('lang', prefs.language);
}

// Apply on load
applyPrefs(loadPrefs());

// Listen for changes in other tabs
window.addEventListener('storage', e => {
  if (e.key === PREFS_KEY) applyPrefs(loadPrefs());
});

4. Code Example

<div>
  <h2>Note Pad</h2>
  <textarea id="note" rows="8" placeholder="Your note..."></textarea>
  <p id="status"></p>
  <button id="clear-btn">Clear Note</button>
  <h3>Theme</h3>
  <button id="theme-btn">Toggle Dark Mode</button>
</div>

<script>
  const NOTE_KEY  = 'notepad_content';
  const THEME_KEY = 'notepad_theme';

  const textarea  = document.querySelector('#note');
  const status    = document.querySelector('#status');
  const clearBtn  = document.querySelector('#clear-btn');
  const themeBtn  = document.querySelector('#theme-btn');

  // Load saved note
  textarea.value = localStorage.getItem(NOTE_KEY) ?? '';

  // Load saved theme
  const savedTheme = localStorage.getItem(THEME_KEY) ?? 'light';
  if (savedTheme === 'dark') document.body.classList.add('dark');

  // Debounce save
  let saveTimer;
  textarea.addEventListener('input', () => {
    clearTimeout(saveTimer);
    status.textContent = 'Saving...';
    saveTimer = setTimeout(() => {
      localStorage.setItem(NOTE_KEY, textarea.value);
      status.textContent = `Saved at ${new Date().toLocaleTimeString()}`;
    }, 500);
  });

  clearBtn.addEventListener('click', () => {
    if (!confirm('Clear the note?')) return;
    textarea.value = '';
    localStorage.removeItem(NOTE_KEY);
    status.textContent = 'Cleared.';
  });

  themeBtn.addEventListener('click', () => {
    const isDark = document.body.classList.toggle('dark');
    localStorage.setItem(THEME_KEY, isDark ? 'dark' : 'light');
  });

  // Show storage info
  console.log('Storage usage:', getStorageUsage());
  function getStorageUsage() {
    let bytes = 0;
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      bytes += k.length + (localStorage.getItem(k)?.length ?? 0);
    }
    return `${bytes} chars (~${(bytes / 1024).toFixed(2)} KB)`;
  }
</script>

5. Code Breakdown

Debounced saving

Saving on every keystroke is unnecessary. The debounce pattern (clearTimeout + setTimeout) waits until the user stops typing for 500ms before saving. The status message reflects the pending → saved transition.

Null coalescing fallback

localStorage.getItem(key) ?? '' — if the key doesn't exist, getItem returns null. The ?? operator replaces null/undefined with the default. Using || here would also replace empty string, which is wrong.

Confirm before destructive action

The clear button shows a native confirmation dialog. This is a good UX pattern for irreversible actions — especially when the data can't be recovered.

6. Common Mistakes

Mistake 1 — Storing objects without JSON

localStorage.setItem('user', { name: 'Alice' });
// Stored as "[object Object]" — useless!

// Fix
localStorage.setItem('user', JSON.stringify({ name: 'Alice' }));

Mistake 2 — Not handling parse errors

// Bug — crashes if data is corrupt
const user = JSON.parse(localStorage.getItem('user')); // throws if null or invalid

// Fix
function load(key, defaultValue = null) {
  try {
    const raw = localStorage.getItem(key);
    return raw !== null ? JSON.parse(raw) : defaultValue;
  } catch { return defaultValue; }
}

Mistake 3 — Storing sensitive data

// Never store passwords, full credit card numbers, or private keys in localStorage
// localStorage is accessible to any JavaScript on the page
// If your site has XSS, an attacker can read all localStorage

// For auth tokens: httpOnly cookies are safer than localStorage
// If you must store tokens in localStorage, understand the risk

Mistake 4 — Blocking the main thread with large data

// localStorage is synchronous — reading/writing large data blocks the UI
// For large datasets, use IndexedDB (async) instead
// Rule of thumb: localStorage is fine for small preferences and state (<100KB)

7. Best Practices

  1. Always use JSON.stringify/parse for non-string values.
  2. Wrap reads in try/catch — data can be corrupt or storage unavailable.
  3. Use namespaced keys (e.g., myapp_theme) to avoid collisions with other scripts.
  4. Never store passwords or sensitive PII in localStorage.
  5. Provide default values when loading — keys may not exist on first run.
  6. Use sessionStorage for data that shouldn't survive closing the tab (e.g., form progress).

8. Practice Exercise

  1. Build a simple to-do list that saves to localStorage. On page refresh, todos should still be there.
  2. Add a theme toggle (light/dark) that persists across refreshes.
  3. Add a "Last visited" feature: store the current timestamp on every page load and display "Last visited: X minutes ago".

9. Assignment

Build a "Reading List Manager" with full localStorage persistence.

  1. Add books: title, author, status (to-read/reading/finished), rating (1–5 stars when finished).
  2. All data stored in localStorage — survives refresh, close, and reopen.
  3. Filter by status. Sort by title or date added.
  4. Export as JSON (download as a .json file using a Blob URL) and Import from a JSON file (using a file input).
  5. Show reading stats: total books, books by status, average rating of finished books.

Deliverable: One HTML file.

10. Interview Questions

  1. What is localStorage?
    A browser Web Storage API that stores key-value pairs as strings, persisting across page reloads and browser sessions until explicitly cleared. Limited to ~5-10MB per origin.
  2. What is the difference between localStorage and sessionStorage?
    localStorage persists forever (until cleared) and is shared across all tabs for the same origin. sessionStorage persists only for the current browser session (cleared when the tab closes) and is isolated per tab.
  3. Why do you use JSON with localStorage?
    localStorage only stores strings. JSON.stringify converts objects/arrays to strings for saving; JSON.parse converts them back to JavaScript values when loading.
  4. What security concerns exist with localStorage?
    Any JavaScript running on the page can access localStorage — including scripts from XSS attacks. Never store passwords, private keys, or sensitive personal data. For authentication tokens, HttpOnly cookies are more secure because they're not accessible via JavaScript.

11. Additional Resources

  • MDN — Web Storage API
  • MDN — localStorage
  • MDN — sessionStorage
  • MDN — storage event