Skip to main content

Client-Side Storage

localStorage

localStorage stores strings persistently — survives page refreshes and browser restarts.

// Store (must serialize objects to JSON)
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));

// Retrieve
const theme = localStorage.getItem('theme'); // 'dark' or null
const raw = localStorage.getItem('user');
const user = raw ? JSON.parse(raw) : null;

// Remove
localStorage.removeItem('theme');

// Clear all
localStorage.clear();

Type-Safe localStorage Wrapper

// src/storage.ts

function get<T>(key: string): T | null {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}

function set<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}

function remove(key: string): void {
localStorage.removeItem(key);
}

export const storage = { get, set, remove };
// Usage
import { storage } from './storage';

storage.set('todos', todos);
const saved = storage.get<Todo[]>('todos') ?? [];

Storage Events (Cross-Tab Sync)

// Listen for changes made in OTHER tabs
window.addEventListener('storage', (e: StorageEvent) => {
if (e.key === 'cart') {
const cart = JSON.parse(e.newValue ?? '[]');
updateCartUI(cart);
}
});

sessionStorage

Same API as localStorage, but data is cleared when the tab/browser closes:

sessionStorage.setItem('draft', JSON.stringify(formData));
const draft = sessionStorage.getItem('draft');

Cookies (when to use)

StoragePersistsSizeSent to serverUse Case
localStorageUntil cleared~5MBNoUI preferences, cached data
sessionStorageTab session~5MBNoMulti-step forms, temp state
CookieConfigurable~4KBYesAuth tokens (httpOnly), session
IndexedDBUntil clearedHundreds MBNoOffline data, files

IndexedDB

IndexedDB is a full client-side database — supports large amounts of structured data, transactions, indexes, and works offline.

The raw API is verbose. Use the idb wrapper library:

npm install idb
import { openDB } from 'idb';

// Open or create database
const db = await openDB('my-app', 1, {
upgrade(db) {
// Create object stores (like tables) on first run or version bump
const todoStore = db.createObjectStore('todos', {
keyPath: 'id',
autoIncrement: false,
});
todoStore.createIndex('by-status', 'status');
},
});

// CRUD operations
await db.add('todos', { id: crypto.randomUUID(), text: 'Learn IDB', status: 'pending' });
await db.put('todos', { id: '123', text: 'Updated', status: 'done' }); // add or update
const todo = await db.get('todos', '123');
const all = await db.getAll('todos');
await db.delete('todos', '123');

// Query by index
const pending = await db.getAllFromIndex('todos', 'by-status', 'pending');

// Transactions
const tx = db.transaction('todos', 'readwrite');
await tx.store.put(todoA);
await tx.store.put(todoB);
await tx.done;

When to Use IndexedDB vs localStorage

Use localStorage for:

  • User preferences (theme, language)
  • Cached API responses (small)
  • Simple key-value state

Use IndexedDB for:

  • Offline-capable apps
  • Large datasets (images, files, many records)
  • Need to query or filter data
  • PWAs with background sync