ข้ามไปเนื้อหาหลัก

Category: guide

Browser Storage — localStorage, sessionStorage, IndexedDB

เปรียบเทียบและใช้งาน browser storage: localStorage สำหรับ simple KV, IndexedDB สำหรับ structured data ขนาดใหญ่

· อ่านประมาณ 4 นาที

สารบัญ

เปรียบเทียบ Storage APIs

localStoragesessionStorageIndexedDBCookies
Capacity~5–10 MB~5 MB≥50 MB~4 KB
Persistenceถาวรปิด tab = ลบถาวรตาม expires
Typestring onlystring onlystructured datastring
Async❌ sync (blocking)❌ sync✓ async❌ sync
Web Workers
Scopesame originsame tabsame originconfigurable

localStorage — Simple Key-Value

// เก็บ
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));

// อ่าน
const theme = localStorage.getItem('theme');  // 'dark' | null
const user = JSON.parse(localStorage.getItem('user') ?? 'null');

// ลบ
localStorage.removeItem('theme');
localStorage.clear();  // ลบทั้งหมด

// ดูทั้งหมด
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key!);
}

// listen for changes จาก tab อื่น (ไม่ fire ใน tab ที่เปลี่ยน)
window.addEventListener('storage', (event) => {
  console.log(event.key, event.oldValue, event.newValue);
});

Typed localStorage Wrapper

function createStorage<T extends Record<string, unknown>>(prefix: string) {
  return {
    get<K extends keyof T>(key: K): T[K] | null {
      const raw = localStorage.getItem(`${prefix}:${String(key)}`);
      if (raw === null) return null;
      return JSON.parse(raw) as T[K];
    },
    set<K extends keyof T>(key: K, value: T[K]): void {
      localStorage.setItem(`${prefix}:${String(key)}`, JSON.stringify(value));
    },
    remove<K extends keyof T>(key: K): void {
      localStorage.removeItem(`${prefix}:${String(key)}`);
    },
  };
}

// ใช้งาน
const store = createStorage<{
  theme: 'light' | 'dark';
  fontSize: number;
  user: { id: string; name: string };
}>('myapp');

store.set('theme', 'dark');         // ✓
store.set('theme', 'blurple');      // ❌ TypeScript error
store.get('theme');                 // 'light' | 'dark' | null

IndexedDB — Structured Data ขนาดใหญ่

IndexedDB ซับซ้อนกว่า localStorage แต่จำเป็นเมื่อ:

  • ข้อมูลมากกว่า 5MB
  • ต้องการ query / filter
  • ต้องการ indexes
  • ต้องการทำงานใน Web Worker

ใช้ผ่าน idb library (Wrapper ที่ดีที่สุด)

npm install idb
import { openDB, type DBSchema } from 'idb';

interface AppDB extends DBSchema {
  notes: {
    key: string;
    value: { id: string; title: string; content: string; createdAt: number };
    indexes: { 'by-date': number };
  };
  settings: {
    key: string;
    value: unknown;
  };
}

// เปิด database (สร้างถ้าไม่มี)
const db = await openDB<AppDB>('my-app', 1, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      const noteStore = db.createObjectStore('notes', { keyPath: 'id' });
      noteStore.createIndex('by-date', 'createdAt');
      db.createObjectStore('settings');
    }
  },
});

// CRUD
await db.put('notes', {
  id: crypto.randomUUID(),
  title: 'Meeting notes',
  content: '...',
  createdAt: Date.now(),
});

const note = await db.get('notes', 'some-id');

// Query ด้วย index
const allNotes = await db.getAllFromIndex('notes', 'by-date');

// Delete
await db.delete('notes', 'some-id');

// Count
const count = await db.count('notes');

Raw IndexedDB API (ไม่ใช้ library)

function openDatabase(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('MyDB', 1);

    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      if (!db.objectStoreNames.contains('items')) {
        db.createObjectStore('items', { keyPath: 'id', autoIncrement: true });
      }
    };

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

async function addItem(db: IDBDatabase, data: object): Promise<number> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('items', 'readwrite');
    const store = tx.objectStore('items');
    const request = store.add(data);
    request.onsuccess = () => resolve(request.result as number);
    request.onerror = () => reject(request.error);
  });
}

Offline-first Pattern

// Cache API สำหรับ network resources (ใช้ใน Service Worker)
const cache = await caches.open('v1');
await cache.add('/api/data');
const response = await cache.match('/api/data');

// Pattern: Cache first, Network fallback
async function fetchWithCache(url: string): Promise<Response> {
  const cached = await caches.match(url);
  if (cached) return cached;

  const response = await fetch(url);
  const cache = await caches.open('v1');
  cache.put(url, response.clone());
  return response;
}

// Pattern: Network first, Cache fallback
async function fetchFreshFirst(url: string): Promise<Response> {
  try {
    const response = await fetch(url);
    const cache = await caches.open('v1');
    cache.put(url, response.clone());
    return response;
  } catch {
    const cached = await caches.match(url);
    if (cached) return cached;
    throw new Error('Offline and no cache available');
  }
}

Storage Quota

// ตรวจสอบ storage quota
if ('storage' in navigator && 'estimate' in navigator.storage) {
  const { usage, quota } = await navigator.storage.estimate();
  const usageMB = (usage ?? 0) / 1024 / 1024;
  const quotaMB = (quota ?? 0) / 1024 / 1024;
  console.log(`Used: ${usageMB.toFixed(1)}MB / ${quotaMB.toFixed(0)}MB`);
}

// ขอ persistent storage (ไม่ถูก evict โดย browser)
const isPersistent = await navigator.storage.persist();
console.log(isPersistent ? 'Storage will persist' : 'Storage may be cleared');

เลือกใช้อะไร

ข้อมูลเล็ก (<100KB) + อ่านเขียนบ่อย + ต้องอยู่ข้าม tabs
→ localStorage

ข้อมูลชั่วคราวใน session เดียว
→ sessionStorage

ข้อมูลมาก / ต้องการ query / offline-first
→ IndexedDB (ใช้ผ่าน idb library)

API responses ที่ cache ได้ (network layer)
→ Cache API (ใน Service Worker)

Auth tokens, session cookies
→ httpOnly cookies (ไม่ใช้ localStorage — XSS เข้าถึงได้)