Local Storage and Session Storage: Browser Data Safe
Evolution of Client-side Storage
In the early days of web development, if you wanted to store a little data in the user's browser, Cookie was almost the only choice. But Cookies have many limitations: small capacity (~4KB), automatically sent to server with every request, cumbersome API.
HTML5 brought the Web Storage API, like a small database customized for browsers—larger capacity (typically 5-10MB), simple operations, and doesn't automatically send to server. This makes client-side data storage elegant and efficient.
Web Storage comes in two flavors: localStorage provides persistent storage where data remains until explicitly deleted; sessionStorage is bound to the session, where data disappears when the tab or browser closes.
localStorage: Persistent Data Warehouse
localStorage is your permanent storage locker in the browser. As long as the user doesn't actively clear browser data, stored content will remain.
Basic Operations
// Store data
localStorage.setItem("username", "Sarah");
localStorage.setItem("theme", "dark");
// Read data
const username = localStorage.getItem("username");
console.log(username); // 'Sarah'
// Reading non-existent key returns null
const notExist = localStorage.getItem("notExist");
console.log(notExist); // null
// Delete single data
localStorage.removeItem("theme");
// Clear all data
localStorage.clear();
// Get number of stored keys
console.log(localStorage.length);
// Get key name by index
const firstKey = localStorage.key(0);
console.log(firstKey);You can also use dot notation or bracket syntax to access directly, though this is not recommended:
// Works but not recommended
localStorage.username = "Michael";
console.log(localStorage.username);
// This approach has risks
localStorage.setItem = "oops"; // This will overwrite the setItem method!
// Always use API methods for safety
localStorage.setItem("setItem", "safe"); // Correct wayStoring Complex Data
localStorage can only store strings. If you try to store other data types, they'll be automatically converted to strings:
// Numbers get converted to strings
localStorage.setItem("count", 42);
console.log(localStorage.getItem("count")); // '42' (string)
console.log(typeof localStorage.getItem("count")); // 'string'
// Booleans too
localStorage.setItem("isActive", true);
console.log(localStorage.getItem("isActive")); // 'true' (string)
// Objects become [object Object]
localStorage.setItem("user", { name: "John" });
console.log(localStorage.getItem("user")); // '[object Object]'To correctly store objects and arrays, you need to use JSON:
// Store object
const user = {
id: 1,
name: "Sarah Chen",
email: "[email protected]",
preferences: {
theme: "dark",
language: "zh-CN",
},
};
localStorage.setItem("user", JSON.stringify(user));
// Read object
const storedUser = JSON.parse(localStorage.getItem("user"));
console.log(storedUser.name); // 'Sarah Chen'
console.log(storedUser.preferences.theme); // 'dark'
// Store array
const recentSearches = ["JavaScript", "React", "Vue", "Node.js"];
localStorage.setItem("searches", JSON.stringify(recentSearches));
// Read array
const searches = JSON.parse(localStorage.getItem("searches"));
console.log(searches[0]); // 'JavaScript'Safe Read Wrapper
Using JSON.parse directly can throw errors if data is corrupted, so it's recommended to wrap it in a safe read function:
function getStoredItem(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
if (item === null) {
return defaultValue;
}
return JSON.parse(item);
} catch (error) {
console.warn(`Failed to read localStorage "${key}":`, error);
return defaultValue;
}
}
function setStoredItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error(`Failed to write localStorage "${key}":`, error);
return false;
}
}
// Usage example
setStoredItem("settings", { volume: 80, muted: false });
const settings = getStoredItem("settings", { volume: 50, muted: false });
console.log(settings.volume); // 80Iterating All Data
// Method 1: using length and key()
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
// Method 2: using Object.keys()
Object.keys(localStorage).forEach((key) => {
console.log(`${key}: ${localStorage.getItem(key)}`);
});
// Method 3: get all data as object
function getAllLocalStorage() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
data[key] = JSON.parse(localStorage.getItem(key));
} catch {
data[key] = localStorage.getItem(key);
}
}
return data;
}
console.log(getAllLocalStorage());sessionStorage: Session-level Temporary Storage
sessionStorage API is identical to localStorage, differing only in data lifecycle—valid only for current session (tab).
Basic Features
// Store data
sessionStorage.setItem("currentStep", "3");
sessionStorage.setItem(
"formData",
JSON.stringify({
name: "John",
email: "[email protected]",
})
);
// Read data
const step = sessionStorage.getItem("currentStep");
console.log(step); // '3'
// Other operations identical to localStorage
sessionStorage.removeItem("currentStep");
sessionStorage.clear();Definition of Session
Understanding "session" in sessionStorage is important:
// Each tab has independent sessionStorage
// Same website opened in different tabs, their sessionStorage is isolated
// Tab A
sessionStorage.setItem("tabId", "A");
// Tab B (even if same website)
sessionStorage.setItem("tabId", "B");
// Each reads its own data
// Tab A reads 'A'
// Tab B reads 'B'
// Page refresh doesn't clear sessionStorage
// Closing tab clears sessionStorage
// "Reopen closed tabs" might restore sessionStorage (browser implementation varies)Practical Application: Multi-step Form
class MultiStepForm {
constructor(formId) {
this.formId = formId;
this.storageKey = `form_${formId}`;
}
saveProgress(step, data) {
const formData = this.getProgress() || { steps: {} };
formData.currentStep = step;
formData.steps[step] = data;
formData.lastUpdated = Date.now();
sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
}
getProgress() {
const data = sessionStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : null;
}
getCurrentStep() {
const progress = this.getProgress();
return progress ? progress.currentStep : 1;
}
getStepData(step) {
const progress = this.getProgress();
return progress?.steps?.[step] || {};
}
clearProgress() {
sessionStorage.removeItem(this.storageKey);
}
submitForm() {
const progress = this.getProgress();
if (!progress) return null;
// Merge all step data
const allData = Object.values(progress.steps).reduce(
(acc, step) => ({ ...acc, ...step }),
{}
);
// Clear session data after submit
this.clearProgress();
return allData;
}
}
// Usage example
const registrationForm = new MultiStepForm("registration");
// Step 1: Basic info
registrationForm.saveProgress(1, {
firstName: "Sarah",
lastName: "Johnson",
});
// Step 2: Contact info
registrationForm.saveProgress(2, {
email: "[email protected]",
phone: "+1-234-567-8900",
});
// User refreshes page, restore progress
const currentStep = registrationForm.getCurrentStep();
console.log(`Current step: ${currentStep}`);
// Get specific step data
const step1Data = registrationForm.getStepData(1);
console.log(step1Data); // { firstName: 'Sarah', lastName: 'Johnson' }localStorage vs sessionStorage
| Feature | localStorage | sessionStorage |
|---|---|---|
| Lifecycle | Permanent until cleared | Cleared on tab close |
| Scope | Shared across all tabs (same origin) | Current tab only |
| Capacity | Usually 5-10MB | Usually 5-10MB |
| Server send | Doesn't auto send | Doesn't auto send |
Selection Guide
// Scenarios for localStorage
// ✅ User preferences
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "zh-CN");
// ✅ User login status (with security policies)
localStorage.setItem("authToken", "xxx");
// ✅ Cache non-sensitive data
localStorage.setItem("cachedProducts", JSON.stringify(products));
// ✅ Remember user choices
localStorage.setItem("rememberMe", "true");
// ---
// Scenarios for sessionStorage
// ✅ Single-session temporary data
sessionStorage.setItem("currentOrder", JSON.stringify(order));
// ✅ Form drafts
sessionStorage.setItem("draftPost", JSON.stringify(postContent));
// ✅ One-time data transfer between pages
sessionStorage.setItem("redirectReason", "login_required");
// ✅ Prevent form duplicate submission
sessionStorage.setItem("formSubmitted", "true");Storage Event Listening
When localStorage is modified in other tabs or windows, you can listen to these changes via storage event:
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 URL triggering change
storageArea: event.storageArea, // localStorage or sessionStorage
});
});
// Note: storage event only triggers when modified in other tabs/windows
// Current page modifying localStorage won't trigger its own storage eventCross-tab Communication
Using storage event, you can implement simple cross-tab communication:
class CrossTabMessenger {
constructor(channelName) {
this.channelName = channelName;
this.callbacks = new Set();
window.addEventListener("storage", (event) => {
if (event.key === this.channelName && event.newValue) {
const message = JSON.parse(event.newValue);
this.callbacks.forEach((callback) => callback(message));
}
});
}
send(data) {
const message = {
data,
timestamp: Date.now(),
tabId: this.tabId,
};
// Set message
localStorage.setItem(this.channelName, JSON.stringify(message));
// Immediately delete to avoid occupying space
localStorage.removeItem(this.channelName);
}
onMessage(callback) {
this.callbacks.add(callback);
return () => this.callbacks.delete(callback);
}
}
// Usage example
const messenger = new CrossTabMessenger("app_messages");
// Listen for messages
messenger.onMessage((message) => {
console.log("Message received:", message.data);
});
// Send message (received by other tabs)
messenger.send({ type: "logout", reason: "user_initiated" });Real-time User State Sync
class UserStateSync {
constructor() {
this.stateKey = "user_state";
this.listeners = [];
// Listen for state changes in other tabs
window.addEventListener("storage", (event) => {
if (event.key === this.stateKey) {
const newState = event.newValue ? JSON.parse(event.newValue) : null;
this.notifyListeners(newState, "external");
}
});
}
setState(state) {
localStorage.setItem(this.stateKey, JSON.stringify(state));
this.notifyListeners(state, "internal");
}
getState() {
const data = localStorage.getItem(this.stateKey);
return data ? JSON.parse(data) : null;
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
notifyListeners(state, source) {
this.listeners.forEach((listener) => listener(state, source));
}
}
// Usage example
const userSync = new UserStateSync();
// Subscribe to state changes
userSync.subscribe((state, source) => {
if (source === "external") {
console.log("State updated by other tab:", state);
// Update current page UI
if (state?.isLoggedIn === false) {
// User logged out in other tab
window.location.href = "/login";
}
}
});
// After login, set state
userSync.setState({
isLoggedIn: true,
username: "Sarah",
lastActive: Date.now(),
});
// On logout
function logout() {
userSync.setState({ isLoggedIn: false });
// All tabs will receive notification and redirect to login
}Storage Limits and Error Handling
Web Storage has capacity limits and throws errors when space is insufficient:
// Detect storage capacity
function getStorageSize(storage) {
let total = 0;
for (let key in storage) {
if (storage.hasOwnProperty(key)) {
total += key.length + storage.getItem(key).length;
}
}
return total;
}
console.log(`localStorage used: ${getStorageSize(localStorage)} chars`);
// Safe storage write
function safeSetItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return { success: true };
} catch (error) {
if (error.name === "QuotaExceededError") {
return { success: false, error: "QUOTA_EXCEEDED" };
}
return { success: false, error: error.message };
}
}
// Storage with auto-cleanup
function setItemWithCleanup(key, value, cleanupKeys = []) {
const result = safeSetItem(key, value);
if (!result.success && result.error === "QUOTA_EXCEEDED") {
// Try to clean expired or unimportant data
cleanupKeys.forEach((k) => localStorage.removeItem(k));
// Retry
return safeSetItem(key, value);
}
return result;
}Storage with Expiration
Native Web Storage doesn't support expiration, but you can implement it yourself:
class ExpiringStorage {
static set(key, value, ttlMs) {
const item = {
value,
expiry: ttlMs ? Date.now() + ttlMs : null,
};
localStorage.setItem(key, JSON.stringify(item));
}
static get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
try {
const item = JSON.parse(itemStr);
// Check if expired
if (item.expiry && Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch {
return null;
}
}
static remove(key) {
localStorage.removeItem(key);
}
// Clean all expired items
static cleanup() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const item = JSON.parse(localStorage.getItem(key));
if (item.expiry && Date.now() > item.expiry) {
keysToRemove.push(key);
}
} catch {
// Not our data format, skip
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
return keysToRemove.length;
}
}
// Usage example
// Cache data for 1 hour
ExpiringStorage.set("apiResponse", { data: [1, 2, 3] }, 60 * 60 * 1000);
// Get data (returns null if expired)
const cached = ExpiringStorage.get("apiResponse");
// Periodically clean expired data
setInterval(() => {
const cleaned = ExpiringStorage.cleanup();
if (cleaned > 0) {
console.log(`Cleaned ${cleaned} expired items`);
}
}, 60 * 1000); // Check every minutePractical Application Scenarios
User Preferences
class UserPreferences {
constructor() {
this.storageKey = "userPreferences";
this.defaults = {
theme: "light",
fontSize: "medium",
language: "en",
notifications: true,
sidebar: "expanded",
};
}
get(key) {
const prefs = this.getAll();
return prefs[key];
}
set(key, value) {
const prefs = this.getAll();
prefs[key] = value;
localStorage.setItem(this.storageKey, JSON.stringify(prefs));
// Trigger change event
window.dispatchEvent(
new CustomEvent("preferencesChanged", {
detail: { key, value },
})
);
}
getAll() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored
? { ...this.defaults, ...JSON.parse(stored) }
: this.defaults;
} catch {
return this.defaults;
}
}
reset() {
localStorage.removeItem(this.storageKey);
window.dispatchEvent(
new CustomEvent("preferencesChanged", {
detail: { reset: true },
})
);
}
}
// Usage example
const prefs = new UserPreferences();
// Get setting
console.log(prefs.get("theme")); // 'light'
// Update setting
prefs.set("theme", "dark");
prefs.set("fontSize", "large");
// Listen for setting changes
window.addEventListener("preferencesChanged", (event) => {
const { key, value, reset } = event.detail;
if (reset) {
console.log("Settings reset");
// Reapply default settings
} else {
console.log(`Setting updated: ${key} = ${value}`);
// Apply new setting
if (key === "theme") {
document.body.className = value;
}
}
});Shopping Cart
class ShoppingCart {
constructor() {
this.storageKey = "shopping_cart";
}
getItems() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
addItem(product, quantity = 1) {
const items = this.getItems();
const existingIndex = items.findIndex((item) => item.id === product.id);
if (existingIndex >= 0) {
items[existingIndex].quantity += quantity;
} else {
items.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity,
});
}
this.save(items);
return items;
}
updateQuantity(productId, quantity) {
const items = this.getItems();
const index = items.findIndex((item) => item.id === productId);
if (index >= 0) {
if (quantity <= 0) {
items.splice(index, 1);
} else {
items[index].quantity = quantity;
}
this.save(items);
}
return items;
}
removeItem(productId) {
const items = this.getItems().filter((item) => item.id !== productId);
this.save(items);
return items;
}
clear() {
localStorage.removeItem(this.storageKey);
}
save(items) {
localStorage.setItem(this.storageKey, JSON.stringify(items));
}
getTotal() {
const items = this.getItems();
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
getItemCount() {
const items = this.getItems();
return items.reduce((count, item) => count + item.quantity, 0);
}
}
// Usage example
const cart = new ShoppingCart();
// Add items
cart.addItem({ id: 1, name: "Wireless Mouse", price: 29.99 }, 2);
cart.addItem({ id: 2, name: "USB Keyboard", price: 49.99 }, 1);
// Get cart info
console.log("Item count:", cart.getItemCount()); // 3
console.log("Total:", cart.getTotal().toFixed(2)); // 109.97
// Update quantity
cart.updateQuantity(1, 3);
// Remove item
cart.removeItem(2);Recently Viewed
class RecentlyViewed {
constructor(options = {}) {
this.storageKey = options.key || "recently_viewed";
this.maxItems = options.maxItems || 10;
}
add(item) {
const items = this.getAll();
// Remove existing same item
const filtered = items.filter((i) => i.id !== item.id);
// Add to beginning
filtered.unshift({
...item,
viewedAt: Date.now(),
});
// Maintain max item limit
const limited = filtered.slice(0, this.maxItems);
localStorage.setItem(this.storageKey, JSON.stringify(limited));
return limited;
}
getAll() {
try {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
clear() {
localStorage.removeItem(this.storageKey);
}
remove(id) {
const items = this.getAll().filter((item) => item.id !== id);
localStorage.setItem(this.storageKey, JSON.stringify(items));
return items;
}
}
// Usage example
const recentlyViewed = new RecentlyViewed({ maxItems: 5 });
// User views product
recentlyViewed.add({ id: 101, name: "Product A", price: 99.99 });
recentlyViewed.add({ id: 102, name: "Product B", price: 149.99 });
recentlyViewed.add({ id: 103, name: "Product C", price: 79.99 });
// Show recently viewed
const recent = recentlyViewed.getAll();
console.log("Recently viewed:", recent);Security Considerations
Although Web Storage is convenient, there are security risks to be aware of:
// ⚠️ Don't store sensitive information
// Following are wrong examples:
localStorage.setItem("password", "mySecret123"); // ❌ NEVER do this
localStorage.setItem("creditCard", "4111111111111111"); // ❌ NEVER do this
// ⚠️ XSS attacks can access localStorage
// If website has XSS vulnerability, attacker can execute:
// console.log(localStorage.getItem('authToken'));
// ✅ Security recommendations:
// 1. Validate stored data
function getValidatedItem(key, validator) {
try {
const value = JSON.parse(localStorage.getItem(key));
if (validator(value)) {
return value;
}
localStorage.removeItem(key); // Invalid data, clear it
return null;
} catch {
return null;
}
}
// 2. For sensitive operations use httpOnly Cookie, not localStorage
// 3. Implement Content Security Policy (CSP) to prevent XSS
// 4. Strictly validate and sanitize user inputSummary
Web Storage API provides a simple and powerful solution for client-side data storage. localStorage is suitable for long-term storage needs like user preferences and cached data; sessionStorage is suitable for session-level storage scenarios like temporary data and form drafts.
Key takeaways:
- Can only store strings, complex data needs JSON serialization
localStorageshared across tabs,sessionStorageisolated per tab- Use
storageevent for cross-tab communication - Watch storage capacity limits, handle errors properly
- Don't store sensitive information, beware of XSS protection
Reasonable use of Web Storage can significantly improve user experience, making your web application smarter and more personalized.