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
- localStorage: Persists across sessions, shared across tabs
- sessionStorage: Per-tab, cleared when tab closes
- Strings Only: Use JSON.stringify/parse for objects and arrays
- 5-10MB Limit: Handle QuotaExceededError gracefully
- Synchronous: Blocks main thread, use sparingly
- Storage Events: Only fire in other tabs, not same tab
- No Expiration: Implement TTL manually if needed
- Security: Never store passwords, tokens should expire