💾 Web Storage

Client-Side Data Storage

What is Web Storage?

Web Storage provides mechanisms for storing key-value pairs in the browser. It includes localStorage (persistent) and sessionStorage (per-session).

🗄️ localStorage vs sessionStorage

// localStorage - persists across browser sessions
localStorage.setItem('username', 'John');
console.log(localStorage.getItem('username'));  // 'John'

// sessionStorage - cleared when tab closes
sessionStorage.setItem('tempData', 'value');
console.log(sessionStorage.getItem('tempData'));  // 'value'

// Key differences:
// localStorage:
// - Persists even after browser closes
// - Shared across all tabs/windows of same origin
// - No expiration time
// - ~5-10MB storage limit (varies by browser)

// sessionStorage:
// - Cleared when tab/window closes
// - Separate for each tab/window
// - Survives page reloads
// - ~5-10MB storage limit (varies by browser)

// Both:
// - Synchronous API
// - Store strings only
// - Same-origin policy
// - Available in all modern browsers

📝 Basic Operations

Storing Data

// setItem(key, value)
localStorage.setItem('username', 'John');
localStorage.setItem('age', '30');
localStorage.setItem('isLoggedIn', 'true');

// Alternative bracket notation
localStorage['username'] = 'John';

// Alternative property access
localStorage.username = 'John';

// Store numbers (converted to string)
localStorage.setItem('count', 42);
console.log(typeof localStorage.getItem('count'));  // 'string'

// Store booleans (converted to string)
localStorage.setItem('active', true);
console.log(localStorage.getItem('active'));  // 'true' (string)

Retrieving Data

// getItem(key)
let username = localStorage.getItem('username');
console.log(username);  // 'John'

// Returns null if key doesn't exist
let missing = localStorage.getItem('nonexistent');
console.log(missing);  // null

// Alternative bracket notation
let age = localStorage['age'];

// Alternative property access
let isLoggedIn = localStorage.isLoggedIn;

// Convert back to original types
let count = parseInt(localStorage.getItem('count'));
let price = parseFloat(localStorage.getItem('price'));
let active = localStorage.getItem('active') === 'true';

// Default values
let theme = localStorage.getItem('theme') || 'light';
let fontSize = parseInt(localStorage.getItem('fontSize')) || 16;

Removing Data

// Remove single item
localStorage.removeItem('username');
console.log(localStorage.getItem('username'));  // null

// Remove all items
localStorage.clear();
console.log(localStorage.length);  // 0

// Check if key exists
if (localStorage.getItem('username') !== null) {
    console.log('Username exists');
}

// Alternative using hasOwnProperty
if (localStorage.hasOwnProperty('username')) {
    console.log('Username exists');
}

🔢 Working with Complex Data

Storing Objects and Arrays

// Store objects with JSON
let user = {
    name: 'John',
    age: 30,
    email: 'john@example.com'
};

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

// Retrieve and parse
let storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.name);  // 'John'

// Store arrays
let todos = [
    { id: 1, text: 'Learn JavaScript', done: true },
    { id: 2, text: 'Build project', done: false }
];

localStorage.setItem('todos', JSON.stringify(todos));

// Retrieve array
let storedTodos = JSON.parse(localStorage.getItem('todos'));
console.log(storedTodos[0].text);  // 'Learn JavaScript'

// Helper functions
function setObject(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
}

function getObject(key) {
    let value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
}

// Usage
setObject('settings', { theme: 'dark', fontSize: 16 });
let settings = getObject('settings');  // { theme: 'dark', fontSize: 16 }

Handling JSON Errors

// Safe JSON parsing
function safeGetObject(key, defaultValue = null) {
    try {
        let value = localStorage.getItem(key);
        return value ? JSON.parse(value) : defaultValue;
    } catch (error) {
        console.error('JSON parse error:', error);
        return defaultValue;
    }
}

// Usage
let settings = safeGetObject('settings', { theme: 'light' });

// Validate stored data
function getUser() {
    let user = safeGetObject('user');
    
    if (!user || !user.name || !user.email) {
        return null;
    }
    
    return user;
}

🔄 Iteration and Enumeration

// Get number of items
console.log(localStorage.length);  // Number of keys

// Get key by index
for (let i = 0; i < localStorage.length; i++) {
    let key = localStorage.key(i);
    let value = localStorage.getItem(key);
    console.log(`${key}: ${value}`);
}

// for...in loop
for (let key in localStorage) {
    if (localStorage.hasOwnProperty(key)) {
        console.log(`${key}: ${localStorage[key]}`);
    }
}

// Object.keys()
Object.keys(localStorage).forEach(key => {
    console.log(`${key}: ${localStorage.getItem(key)}`);
});

// Get all items as object
function getAllStorage() {
    let items = {};
    for (let i = 0; i < localStorage.length; i++) {
        let key = localStorage.key(i);
        items[key] = localStorage.getItem(key);
    }
    return items;
}

console.log(getAllStorage());

// Filter keys by prefix
function getByPrefix(prefix) {
    let items = {};
    for (let i = 0; i < localStorage.length; i++) {
        let key = localStorage.key(i);
        if (key.startsWith(prefix)) {
            items[key] = localStorage.getItem(key);
        }
    }
    return items;
}

let userSettings = getByPrefix('user_');

⏱️ Expiration and TTL

// Store with expiration timestamp
function setWithExpiry(key, value, ttl) {
    let now = new Date();
    let item = {
        value: value,
        expiry: now.getTime() + ttl
    };
    localStorage.setItem(key, JSON.stringify(item));
}

// Get with expiration check
function getWithExpiry(key) {
    let itemStr = localStorage.getItem(key);
    
    if (!itemStr) {
        return null;
    }
    
    let item = JSON.parse(itemStr);
    let now = new Date();
    
    if (now.getTime() > item.expiry) {
        localStorage.removeItem(key);
        return null;
    }
    
    return item.value;
}

// Usage
setWithExpiry('token', 'abc123', 3600000);  // 1 hour
let token = getWithExpiry('token');

// Storage with TTL class
class StorageWithTTL {
    constructor(storage = localStorage) {
        this.storage = storage;
    }
    
    set(key, value, ttl) {
        let item = {
            value,
            expiry: Date.now() + ttl
        };
        this.storage.setItem(key, JSON.stringify(item));
    }
    
    get(key) {
        let itemStr = this.storage.getItem(key);
        
        if (!itemStr) {
            return null;
        }
        
        try {
            let item = JSON.parse(itemStr);
            
            if (Date.now() > item.expiry) {
                this.remove(key);
                return null;
            }
            
            return item.value;
        } catch {
            return null;
        }
    }
    
    remove(key) {
        this.storage.removeItem(key);
    }
    
    clear() {
        this.storage.clear();
    }
    
    // Clean expired items
    cleanup() {
        let keys = Object.keys(this.storage);
        keys.forEach(key => {
            this.get(key);  // Removes if expired
        });
    }
}

// Usage
let cache = new StorageWithTTL();
cache.set('data', { id: 1 }, 60000);  // 1 minute
let data = cache.get('data');
cache.cleanup();  // Remove expired

👂 Storage Events

// Listen for storage changes (from other tabs/windows)
window.addEventListener('storage', event => {
    console.log('Storage changed:');
    console.log('Key:', event.key);
    console.log('Old value:', event.oldValue);
    console.log('New value:', event.newValue);
    console.log('URL:', event.url);
    console.log('Storage:', event.storageArea);
});

// Note: storage event does NOT fire in the same tab that made the change
// Only fires in OTHER tabs/windows of same origin

// Sync data across tabs
window.addEventListener('storage', event => {
    if (event.key === 'user') {
        let user = JSON.parse(event.newValue);
        updateUI(user);
    }
    
    if (event.key === 'theme') {
        applyTheme(event.newValue);
    }
});

// Detect when storage is cleared
window.addEventListener('storage', event => {
    if (event.key === null) {
        console.log('Storage was cleared');
        resetApp();
    }
});

// Custom cross-tab communication
function broadcastMessage(type, data) {
    let message = { type, data, timestamp: Date.now() };
    localStorage.setItem('message', JSON.stringify(message));
    localStorage.removeItem('message');  // Trigger event
}

window.addEventListener('storage', event => {
    if (event.key === 'message' && event.newValue) {
        let message = JSON.parse(event.newValue);
        handleMessage(message);
    }
});

⚠️ Storage Limits and Errors

// Check storage limit (approximate)
function getStorageSize() {
    let total = 0;
    for (let key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
            total += localStorage[key].length + key.length;
        }
    }
    return total;
}

console.log(`Storage used: ${getStorageSize()} characters`);

// Handle quota exceeded error
function safeSetItem(key, value) {
    try {
        localStorage.setItem(key, value);
        return true;
    } catch (error) {
        if (error.name === 'QuotaExceededError') {
            console.error('Storage quota exceeded');
            // Handle: clear old data, notify user, etc.
            return false;
        }
        throw error;
    }
}

// Auto-cleanup when quota exceeded
class SmartStorage {
    constructor() {
        this.prefix = 'app_';
    }
    
    set(key, value) {
        let fullKey = this.prefix + key;
        
        try {
            localStorage.setItem(fullKey, JSON.stringify({
                value,
                timestamp: Date.now()
            }));
        } catch (error) {
            if (error.name === 'QuotaExceededError') {
                this.cleanup();
                // Try again after cleanup
                localStorage.setItem(fullKey, JSON.stringify({
                    value,
                    timestamp: Date.now()
                }));
            }
        }
    }
    
    cleanup() {
        let items = [];
        
        for (let i = 0; i < localStorage.length; i++) {
            let key = localStorage.key(i);
            if (key.startsWith(this.prefix)) {
                let item = JSON.parse(localStorage.getItem(key));
                items.push({ key, timestamp: item.timestamp });
            }
        }
        
        // Sort by timestamp (oldest first)
        items.sort((a, b) => a.timestamp - b.timestamp);
        
        // Remove oldest 25%
        let removeCount = Math.ceil(items.length * 0.25);
        for (let i = 0; i < removeCount; i++) {
            localStorage.removeItem(items[i].key);
        }
    }
}

// Check if storage is available
function isStorageAvailable(type) {
    let storage;
    try {
        storage = window[type];
        let test = '__storage_test__';
        storage.setItem(test, test);
        storage.removeItem(test);
        return true;
    } catch (error) {
        return false;
    }
}

if (isStorageAvailable('localStorage')) {
    console.log('localStorage available');
} else {
    console.log('localStorage not available (private mode?)');
}

💡 Practical Examples

User Preferences

class UserPreferences {
    constructor() {
        this.key = 'user_preferences';
        this.defaults = {
            theme: 'light',
            fontSize: 16,
            language: 'en',
            notifications: true
        };
    }
    
    get(key) {
        let prefs = this.getAll();
        return key ? prefs[key] : prefs;
    }
    
    getAll() {
        let stored = localStorage.getItem(this.key);
        if (!stored) {
            return { ...this.defaults };
        }
        
        try {
            let prefs = JSON.parse(stored);
            return { ...this.defaults, ...prefs };
        } catch {
            return { ...this.defaults };
        }
    }
    
    set(key, value) {
        let prefs = this.getAll();
        prefs[key] = value;
        localStorage.setItem(this.key, JSON.stringify(prefs));
        this.apply(key, value);
    }
    
    apply(key, value) {
        switch (key) {
            case 'theme':
                document.body.className = value;
                break;
            case 'fontSize':
                document.documentElement.style.fontSize = value + 'px';
                break;
            case 'language':
                // Load language pack
                break;
        }
    }
    
    applyAll() {
        let prefs = this.getAll();
        Object.entries(prefs).forEach(([key, value]) => {
            this.apply(key, value);
        });
    }
    
    reset() {
        localStorage.removeItem(this.key);
        this.applyAll();
    }
}

// Usage
let prefs = new UserPreferences();
prefs.applyAll();  // On page load
prefs.set('theme', 'dark');
prefs.set('fontSize', 18);

Form Data Persistence

class FormStorage {
    constructor(formId) {
        this.formId = formId;
        this.form = document.getElementById(formId);
        this.storageKey = `form_${formId}`;
        
        this.restore();
        this.attachListeners();
    }
    
    save() {
        let data = {};
        let formData = new FormData(this.form);
        
        for (let [key, value] of formData.entries()) {
            data[key] = value;
        }
        
        localStorage.setItem(this.storageKey, JSON.stringify(data));
    }
    
    restore() {
        let stored = localStorage.getItem(this.storageKey);
        if (!stored) return;
        
        try {
            let data = JSON.parse(stored);
            
            Object.entries(data).forEach(([name, value]) => {
                let input = this.form.elements[name];
                if (input) {
                    if (input.type === 'checkbox') {
                        input.checked = value === 'on';
                    } else if (input.type === 'radio') {
                        let radio = this.form.querySelector(
                            `input[name="${name}"][value="${value}"]`
                        );
                        if (radio) radio.checked = true;
                    } else {
                        input.value = value;
                    }
                }
            });
        } catch (error) {
            console.error('Restore error:', error);
        }
    }
    
    clear() {
        localStorage.removeItem(this.storageKey);
        this.form.reset();
    }
    
    attachListeners() {
        // Save on input change
        this.form.addEventListener('input', () => {
            this.save();
        });
        
        // Clear on submit
        this.form.addEventListener('submit', () => {
            this.clear();
        });
    }
}

// Usage
let formStorage = new FormStorage('contactForm');

Shopping Cart

class ShoppingCart {
    constructor() {
        this.key = 'shopping_cart';
    }
    
    getItems() {
        let stored = localStorage.getItem(this.key);
        return stored ? JSON.parse(stored) : [];
    }
    
    addItem(product) {
        let items = this.getItems();
        let existing = items.find(item => item.id === product.id);
        
        if (existing) {
            existing.quantity += 1;
        } else {
            items.push({
                ...product,
                quantity: 1,
                addedAt: Date.now()
            });
        }
        
        this.save(items);
        this.notify('added', product);
    }
    
    removeItem(productId) {
        let items = this.getItems();
        items = items.filter(item => item.id !== productId);
        this.save(items);
        this.notify('removed', { id: productId });
    }
    
    updateQuantity(productId, quantity) {
        let items = this.getItems();
        let item = items.find(item => item.id === productId);
        
        if (item) {
            if (quantity <= 0) {
                this.removeItem(productId);
            } else {
                item.quantity = quantity;
                this.save(items);
                this.notify('updated', item);
            }
        }
    }
    
    clear() {
        localStorage.removeItem(this.key);
        this.notify('cleared');
    }
    
    getTotal() {
        let items = this.getItems();
        return items.reduce((sum, item) => {
            return sum + (item.price * item.quantity);
        }, 0);
    }
    
    getCount() {
        let items = this.getItems();
        return items.reduce((sum, item) => sum + item.quantity, 0);
    }
    
    save(items) {
        localStorage.setItem(this.key, JSON.stringify(items));
    }
    
    notify(event, data) {
        window.dispatchEvent(new CustomEvent('cartChange', {
            detail: { event, data, cart: this.getItems() }
        }));
    }
}

// Usage
let cart = new ShoppingCart();

cart.addItem({ id: 1, name: 'Product', price: 29.99 });
cart.updateQuantity(1, 3);

window.addEventListener('cartChange', e => {
    console.log('Cart event:', e.detail.event);
    updateCartDisplay(e.detail.cart);
});

Recently Viewed Items

class RecentlyViewed {
    constructor(maxItems = 10) {
        this.key = 'recently_viewed';
        this.maxItems = maxItems;
    }
    
    add(item) {
        let items = this.getAll();
        
        // Remove if already exists
        items = items.filter(i => i.id !== item.id);
        
        // Add to beginning
        items.unshift({
            ...item,
            viewedAt: Date.now()
        });
        
        // Keep only max items
        items = items.slice(0, this.maxItems);
        
        localStorage.setItem(this.key, JSON.stringify(items));
    }
    
    getAll() {
        let stored = localStorage.getItem(this.key);
        return stored ? JSON.parse(stored) : [];
    }
    
    clear() {
        localStorage.removeItem(this.key);
    }
    
    remove(itemId) {
        let items = this.getAll();
        items = items.filter(item => item.id !== itemId);
        localStorage.setItem(this.key, JSON.stringify(items));
    }
}

// Usage
let recent = new RecentlyViewed(5);
recent.add({ id: 1, name: 'Product A', image: 'a.jpg' });

// Display recent items
let items = recent.getAll();
displayRecentItems(items);

🔒 Security Considerations

// DON'T store sensitive data
// ❌ Bad
localStorage.setItem('password', userPassword);
localStorage.setItem('creditCard', cardNumber);
localStorage.setItem('apiKey', privateKey);

// ✅ Good - Store session tokens (with expiration)
localStorage.setItem('sessionToken', token);

// Sanitize data before storing
function sanitize(data) {
    // Remove sensitive fields
    let { password, creditCard, ssn, ...safe } = data;
    return safe;
}

let user = { name: 'John', email: 'john@example.com', password: 'secret' };
localStorage.setItem('user', JSON.stringify(sanitize(user)));

// Validate data when retrieving
function getUser() {
    let stored = localStorage.getItem('user');
    if (!stored) return null;
    
    try {
        let user = JSON.parse(stored);
        
        // Validate structure
        if (!user.name || !user.email) {
            throw new Error('Invalid user data');
        }
        
        // Sanitize untrusted data
        return {
            name: String(user.name).slice(0, 100),
            email: String(user.email).toLowerCase()
        };
    } catch (error) {
        localStorage.removeItem('user');
        return null;
    }
}

// XSS protection - don't use innerHTML with stored data
let username = localStorage.getItem('username');
element.textContent = username;  // ✅ Safe
element.innerHTML = username;    // ❌ Dangerous if contains HTML

🎯 Key Takeaways