Client Storage
localStorage, sessionStorage, IndexedDB, cookies, and caching strategies
Client Storage
Browsers provide various storage mechanisms for persisting data. Each has different use cases, capacities, and security implications.
Storage Comparison
| Storage | Capacity | Persistence | Scope | Access |
|---|---|---|---|---|
| Cookies | ~4KB | Configurable | Server + Client | HTTP requests |
| localStorage | ~5-10MB | Permanent | Origin | Client only |
| sessionStorage | ~5-10MB | Session (tab) | Origin + Tab | Client only |
| IndexedDB | Large (GBs) | Permanent | Origin | Client only |
| Cache API | Large | Permanent | Origin | Service Worker |
Web Storage API
localStorage
// Store data (synchronous)
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'John' }));
// Retrieve data
const user = JSON.parse(localStorage.getItem('user') || 'null');
// Remove item
localStorage.removeItem('user');
// Clear all
localStorage.clear();
// Check storage availability
function isLocalStorageAvailable(): boolean {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
}
// Type-safe wrapper
class TypedStorage<T> {
constructor(private key: string, private defaultValue: T) {}
get(): T {
try {
const item = localStorage.getItem(this.key);
return item ? JSON.parse(item) : this.defaultValue;
} catch {
return this.defaultValue;
}
}
set(value: T): void {
localStorage.setItem(this.key, JSON.stringify(value));
}
remove(): void {
localStorage.removeItem(this.key);
}
}
// Usage
const themeStorage = new TypedStorage<'light' | 'dark'>('theme', 'light');
themeStorage.set('dark');
const theme = themeStorage.get(); // 'dark'sessionStorage
// Same API as localStorage, but cleared when tab closes
sessionStorage.setItem('tempData', JSON.stringify(data));
// Useful for:
// - Form data backup during navigation
// - Single-session state
// - Tab-specific dataStorage Events
// Listen for storage changes from other tabs
window.addEventListener('storage', (event) => {
if (event.key === 'user') {
console.log('User data changed in another tab');
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
}
});
// Broadcast to other tabs
function broadcastLogout() {
localStorage.setItem('logout', Date.now().toString());
localStorage.removeItem('logout');
}IndexedDB
IndexedDB is a low-level API for storing large amounts of structured data.
Basic Usage
// Open database
function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create object store
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
store.createIndex('name', 'name', { unique: false });
}
};
});
}
// CRUD operations
async function addUser(user: User): Promise<void> {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async function getUser(id: number): Promise<User | undefined> {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getAllUsers(): Promise<User[]> {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function deleteUser(id: number): Promise<void> {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}Using Dexie.js (Recommended)
import Dexie, { Table } from 'dexie';
interface User {
id?: number;
name: string;
email: string;
createdAt: Date;
}
class AppDatabase extends Dexie {
users!: Table<User>;
constructor() {
super('AppDatabase');
this.version(1).stores({
users: '++id, email, name, createdAt',
});
}
}
const db = new AppDatabase();
// CRUD operations
async function createUser(user: Omit<User, 'id'>) {
return db.users.add({ ...user, createdAt: new Date() });
}
async function getUserByEmail(email: string) {
return db.users.where('email').equals(email).first();
}
async function searchUsers(query: string) {
return db.users
.where('name')
.startsWithIgnoreCase(query)
.toArray();
}
async function updateUser(id: number, changes: Partial<User>) {
return db.users.update(id, changes);
}
async function deleteUser(id: number) {
return db.users.delete(id);
}
// Transactions
async function transferData(fromId: number, toId: number) {
await db.transaction('rw', db.users, async () => {
const from = await db.users.get(fromId);
const to = await db.users.get(toId);
// Atomic operations...
});
}Cookies
JavaScript Cookie Access
// Read cookies
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
// Set cookie
function setCookie(
name: string,
value: string,
options: {
expires?: Date | number; // Date or days
path?: string;
domain?: string;
secure?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None';
} = {}
): void {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.expires) {
const date = options.expires instanceof Date
? options.expires
: new Date(Date.now() + options.expires * 864e5);
cookie += `; expires=${date.toUTCString()}`;
}
if (options.path) cookie += `; path=${options.path}`;
if (options.domain) cookie += `; domain=${options.domain}`;
if (options.secure) cookie += '; secure';
if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
document.cookie = cookie;
}
// Delete cookie
function deleteCookie(name: string, path = '/'): void {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
}
// Usage
setCookie('preferences', JSON.stringify({ theme: 'dark' }), {
expires: 365,
path: '/',
secure: true,
sameSite: 'Strict',
});Cookie Security
# Secure cookie settings (set by server)
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400
# HttpOnly prevents JavaScript access (XSS protection)
# Secure ensures HTTPS only
# SameSite prevents CSRFCache API
// Cache API (for Service Workers and offline)
const CACHE_NAME = 'app-v1';
// Cache resources
async function cacheResources(urls: string[]): Promise<void> {
const cache = await caches.open(CACHE_NAME);
await cache.addAll(urls);
}
// Get from cache with network fallback
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}
// Network first with cache fallback
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
throw new Error('No cached response available');
}
}
// Stale while revalidate
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
// Clear old caches
async function clearOldCaches(): Promise<void> {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
);
}Storage Quota
// Check storage quota
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
const usedMB = (usage || 0) / (1024 * 1024);
const quotaMB = (quota || 0) / (1024 * 1024);
console.log(`Using ${usedMB.toFixed(2)} MB of ${quotaMB.toFixed(2)} MB`);
return { usage, quota };
}
}
// Request persistent storage
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const persistent = await navigator.storage.persist();
console.log(`Persistent storage: ${persistent ? 'granted' : 'denied'}`);
return persistent;
}
return false;
}Best Practices
Storage Guidelines
- Use appropriate storage for the use case
- Never store sensitive data in localStorage
- Encrypt sensitive data if stored client-side
- Handle storage quota errors gracefully
- Use IndexedDB for large or structured data
- Implement data migration for schema changes
- Clear stale data periodically
- Use service workers for offline caching
Storage Decision Tree
What should I use?
├── Needs to be sent with HTTP requests?
│ └── Cookies (HttpOnly for security)
├── Large structured data?
│ └── IndexedDB (with Dexie.js)
├── Small key-value data?
│ ├── Persist across sessions?
│ │ └── localStorage
│ └── Only for this session?
│ └── sessionStorage
└── Offline/caching?
└── Cache API (Service Worker)