Browser storage options serve different needs. Here's how to choose and use each effectively.
Storage Comparison#
| Feature | Cookies | localStorage | sessionStorage | IndexedDB |
|----------------|--------------|--------------|----------------|--------------|
| Capacity | 4KB | 5-10MB | 5-10MB | 50MB+ |
| Persistence | Configurable | Permanent | Tab session | Permanent |
| Sent to server | Yes | No | No | No |
| Sync/Async | Sync | Sync | Sync | Async |
| Data type | String | String | String | Any |
localStorage#
1// Simple key-value storage
2localStorage.setItem('theme', 'dark');
3const theme = localStorage.getItem('theme');
4localStorage.removeItem('theme');
5localStorage.clear();
6
7// Store objects (must serialize)
8const user = { id: '123', name: 'John' };
9localStorage.setItem('user', JSON.stringify(user));
10const stored = JSON.parse(localStorage.getItem('user') || 'null');
11
12// Type-safe wrapper
13class LocalStorage<T> {
14 constructor(private key: string) {}
15
16 get(): T | null {
17 const item = localStorage.getItem(this.key);
18 return item ? JSON.parse(item) : null;
19 }
20
21 set(value: T): void {
22 localStorage.setItem(this.key, JSON.stringify(value));
23 }
24
25 remove(): void {
26 localStorage.removeItem(this.key);
27 }
28}
29
30// Usage
31const userStorage = new LocalStorage<User>('user');
32userStorage.set({ id: '123', name: 'John' });
33const user = userStorage.get();
34
35// Listen for changes (cross-tab)
36window.addEventListener('storage', (event) => {
37 if (event.key === 'user') {
38 console.log('User changed:', event.newValue);
39 }
40});sessionStorage#
1// Same API as localStorage, but per-tab
2sessionStorage.setItem('formDraft', JSON.stringify(formData));
3const draft = JSON.parse(sessionStorage.getItem('formDraft') || '{}');
4
5// Good for:
6// - Form data preservation during session
7// - Temporary state
8// - Tab-specific data
9
10// React hook for sessionStorage
11function useSessionStorage<T>(key: string, initialValue: T) {
12 const [value, setValue] = useState<T>(() => {
13 const stored = sessionStorage.getItem(key);
14 return stored ? JSON.parse(stored) : initialValue;
15 });
16
17 useEffect(() => {
18 sessionStorage.setItem(key, JSON.stringify(value));
19 }, [key, value]);
20
21 return [value, setValue] as const;
22}
23
24// Usage
25function CheckoutForm() {
26 const [formData, setFormData] = useSessionStorage('checkout', {
27 name: '',
28 address: '',
29 });
30
31 // Form preserved if user refreshes
32}IndexedDB#
1// Async database for large/complex data
2class Database {
3 private db: IDBDatabase | null = null;
4 private dbName: string;
5 private version: number;
6
7 constructor(dbName: string, version: number = 1) {
8 this.dbName = dbName;
9 this.version = version;
10 }
11
12 async open(): Promise<void> {
13 return new Promise((resolve, reject) => {
14 const request = indexedDB.open(this.dbName, this.version);
15
16 request.onerror = () => reject(request.error);
17 request.onsuccess = () => {
18 this.db = request.result;
19 resolve();
20 };
21
22 request.onupgradeneeded = (event) => {
23 const db = (event.target as IDBOpenDBRequest).result;
24
25 // Create object stores
26 if (!db.objectStoreNames.contains('users')) {
27 db.createObjectStore('users', { keyPath: 'id' });
28 }
29
30 if (!db.objectStoreNames.contains('posts')) {
31 const store = db.createObjectStore('posts', { keyPath: 'id' });
32 store.createIndex('userId', 'userId', { unique: false });
33 store.createIndex('createdAt', 'createdAt', { unique: false });
34 }
35 };
36 });
37 }
38
39 async add<T>(storeName: string, data: T): Promise<void> {
40 return new Promise((resolve, reject) => {
41 const transaction = this.db!.transaction(storeName, 'readwrite');
42 const store = transaction.objectStore(storeName);
43 const request = store.add(data);
44
45 request.onerror = () => reject(request.error);
46 request.onsuccess = () => resolve();
47 });
48 }
49
50 async get<T>(storeName: string, key: string): Promise<T | undefined> {
51 return new Promise((resolve, reject) => {
52 const transaction = this.db!.transaction(storeName, 'readonly');
53 const store = transaction.objectStore(storeName);
54 const request = store.get(key);
55
56 request.onerror = () => reject(request.error);
57 request.onsuccess = () => resolve(request.result);
58 });
59 }
60
61 async getAll<T>(storeName: string): Promise<T[]> {
62 return new Promise((resolve, reject) => {
63 const transaction = this.db!.transaction(storeName, 'readonly');
64 const store = transaction.objectStore(storeName);
65 const request = store.getAll();
66
67 request.onerror = () => reject(request.error);
68 request.onsuccess = () => resolve(request.result);
69 });
70 }
71
72 async put<T>(storeName: string, data: T): Promise<void> {
73 return new Promise((resolve, reject) => {
74 const transaction = this.db!.transaction(storeName, 'readwrite');
75 const store = transaction.objectStore(storeName);
76 const request = store.put(data);
77
78 request.onerror = () => reject(request.error);
79 request.onsuccess = () => resolve();
80 });
81 }
82
83 async delete(storeName: string, key: string): Promise<void> {
84 return new Promise((resolve, reject) => {
85 const transaction = this.db!.transaction(storeName, 'readwrite');
86 const store = transaction.objectStore(storeName);
87 const request = store.delete(key);
88
89 request.onerror = () => reject(request.error);
90 request.onsuccess = () => resolve();
91 });
92 }
93}
94
95// Usage
96const db = new Database('myApp', 1);
97await db.open();
98
99await db.add('users', { id: '123', name: 'John', email: 'john@example.com' });
100const user = await db.get('users', '123');
101const allUsers = await db.getAll('users');Cookies#
1// Set cookie
2document.cookie = 'theme=dark; path=/; max-age=31536000; secure; samesite=strict';
3
4// Parse cookies
5function getCookie(name: string): string | null {
6 const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
7 return match ? decodeURIComponent(match[2]) : null;
8}
9
10// Set cookie helper
11function setCookie(
12 name: string,
13 value: string,
14 options: {
15 maxAge?: number;
16 expires?: Date;
17 path?: string;
18 domain?: string;
19 secure?: boolean;
20 sameSite?: 'strict' | 'lax' | 'none';
21 } = {}
22): void {
23 let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
24
25 if (options.maxAge) cookie += `; max-age=${options.maxAge}`;
26 if (options.expires) cookie += `; expires=${options.expires.toUTCString()}`;
27 if (options.path) cookie += `; path=${options.path}`;
28 if (options.domain) cookie += `; domain=${options.domain}`;
29 if (options.secure) cookie += '; secure';
30 if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
31
32 document.cookie = cookie;
33}
34
35// Delete cookie
36function deleteCookie(name: string): void {
37 document.cookie = `${name}=; max-age=0; path=/`;
38}
39
40// Usage
41setCookie('session', 'abc123', {
42 maxAge: 86400,
43 path: '/',
44 secure: true,
45 sameSite: 'strict',
46});When to Use What#
1// localStorage: Persistent user preferences
2localStorage.setItem('theme', 'dark');
3localStorage.setItem('language', 'en');
4
5// sessionStorage: Temporary form data
6sessionStorage.setItem('checkoutStep', '2');
7sessionStorage.setItem('cartItems', JSON.stringify(items));
8
9// IndexedDB: Large datasets, offline data
10await db.add('articles', { id: '1', content: '...', images: [...] });
11
12// Cookies: Auth tokens, server-readable data
13setCookie('auth_token', token, { secure: true, sameSite: 'strict' });Storage with Expiration#
1interface StorageItem<T> {
2 value: T;
3 expiry: number;
4}
5
6class ExpiringStorage<T> {
7 constructor(private key: string) {}
8
9 set(value: T, ttlMs: number): void {
10 const item: StorageItem<T> = {
11 value,
12 expiry: Date.now() + ttlMs,
13 };
14 localStorage.setItem(this.key, JSON.stringify(item));
15 }
16
17 get(): T | null {
18 const itemStr = localStorage.getItem(this.key);
19 if (!itemStr) return null;
20
21 const item: StorageItem<T> = JSON.parse(itemStr);
22
23 if (Date.now() > item.expiry) {
24 localStorage.removeItem(this.key);
25 return null;
26 }
27
28 return item.value;
29 }
30}
31
32// Usage
33const cache = new ExpiringStorage<User[]>('cachedUsers');
34cache.set(users, 60 * 60 * 1000); // 1 hour
35const cached = cache.get(); // null if expiredBest Practices#
Security:
✓ Never store sensitive data unencrypted
✓ Use httpOnly cookies for auth tokens
✓ Set appropriate cookie flags
✓ Validate stored data
Performance:
✓ Don't store too much in localStorage
✓ Use IndexedDB for large data
✓ Batch IndexedDB operations
✓ Clean up expired data
Reliability:
✓ Handle storage quota errors
✓ Provide fallbacks
✓ Validate JSON parsing
✓ Test private browsing mode
Conclusion#
Choose storage based on data size, persistence needs, and whether the server needs access. localStorage for small persistent data, sessionStorage for tab-specific temporary data, IndexedDB for large datasets, and cookies when the server needs the data.