๐พ Browser Storage Options โ Pick the Right One
Storage Comparison
| Storage | Capacity | Persistence | Accessible from | Use case |
|---|---|---|---|---|
| localStorage | ~5โ10 MB | Until cleared | JS only (same origin) | Theme, settings, tokens |
| sessionStorage | ~5 MB | Tab lifetime | JS only (same origin) | One-time wizard state |
| Cookies | ~4 KB | Configurable expiry | JS + HTTP headers | Auth tokens, session IDs |
| IndexedDB | Hundreds of MB | Until cleared | JS (async API) | Offline data, large datasets |
| Cache API | GBs | Until cleared by SW | JS + Service Worker | PWA asset caching |
| Memory (useState) | RAM limit | Page lifetime | Current component tree | Ephemeral UI state |
Cookies โ Security Flags
HttpOnly Can't be read by JS โ prevents XSS token theft
Secure Only sent over HTTPS
SameSite=Strict Never sent cross-site โ prevents CSRF
SameSite=Lax Sent on top-level navigation GET only
SameSite=None Always sent (must also be Secure)
โ
Auth tokens: HttpOnly + Secure + SameSite=Strict cookie (not localStorage)
IndexedDB
Transactional, async, key-value + index store. Good for offline-first apps.
// Use idb library (wrapper around verbose raw API)
import { openDB } from "idb";
const db = await openDB("mydb", 1, {
upgrade(db) {
db.createObjectStore("notes", { keyPath: "id" });
},
});
await db.put("notes", { id: 1, text: "hello" });
const note = await db.get("notes", 1);
const all = await db.getAll("notes"); State Management Patterns
Local state useState โ component-specific (form fields, toggles)
Shared/lifted Lift to nearest common ancestor
Context API Avoid prop drilling. Re-renders all consumers on change.
Zustand Lightweight global store. Selector-based (no unnecessary re-renders).
Redux Toolkit Predictable, devtools, good for large complex state
React Query / SWR Server state โ fetching, caching, sync. Don't put server data in Redux!
Rule of thumb: Local โ Context โ React Query (server) โ Zustand/Redux (complex
client)
๐ Offline & PWA
Service Worker Caching Strategies
Cache First Serve from cache, fallback to network. Good for static assets.
Network First Try network, fallback to cache. Good for fresh content.
Stale-While-Revalidate Serve cache immediately, update cache in background. Best UX.
Cache Only Serve from cache only. Good for precached app shell.
Network Only No caching. Good for non-cacheable (analytics, forms).
Optimistic UI
Update UI immediately before server confirms. Roll back on error.
// Pattern
function addComment(text) {
const optimistic = { id: uuid(), text, pending: true };
setComments(prev => [...prev, optimistic]); // instant UI
api.postComment(text)
.then(real => setComments(prev =>
prev.map(c => c.id === optimistic.id ? real : c)
))
.catch(() => {
setComments(prev => prev.filter(c => c.id !== optimistic.id));
showError("Failed to post comment");
});
}