Bab 4: Penyimpanan Data Browser

3 menit baca

4.1 Cookies

Cookie adalah potongan kecil data yang disimpan browser dan dikirim otomatis ke server setiap request. Awalnya dibuat untuk "mengingat" user (login, preferensi).

Analogi: Cookie itu kayak gelang tamu di hotel. Setiap kali kamu ke restoran/kolam renang (request ke server), kamu tunjukkan gelang (cookie dikirim otomatis) supaya mereka tahu kamu tamu kamar berapa.

javascript
// Semua cookie dalam satu string
console.log(document.cookie);
// "username=yazid; theme=dark; lang=id"

// Parse jadi object
function getCookies() {
  const cookies = {};
  document.cookie.split(';').forEach(cookie => {
    const [key, value] = cookie.trim().split('=');
    cookies[key] = decodeURIComponent(value);
  });
  return cookies;
}

console.log(getCookies());
// { username: "yazid", theme: "dark", lang: "id" }

// Ambil satu cookie
function getCookie(name) {
  const match = document.cookie.match(
    new RegExp('(?:^|; )' + name + '=([^;]*)')
  );
  return match ? decodeURIComponent(match[1]) : undefined;
}

console.log(getCookie('username')); // "yazid"
javascript
// Set cookie sederhana
document.cookie = "username=yazid";

// ⚠️ Ini TIDAK menghapus cookie lain! Hanya menambah/update satu

// Dengan opsi lengkap
document.cookie = [
  `username=${encodeURIComponent('Yazid Hakim')}`,
  'path=/',              // berlaku di seluruh site
  'max-age=86400',       // expire dalam 24 jam (detik)
  'secure',             // hanya kirim via HTTPS
  'samesite=lax'        // proteksi CSRF
].join('; ');

// Dengan tanggal expire spesifik
const expire = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 hari
document.cookie = `theme=dark; expires=${expire.toUTCString()}; path=/`;
javascript
// path - cookie berlaku di path mana
document.cookie = "admin=true; path=/admin"; // hanya di /admin/*

// domain - cookie berlaku di domain mana
document.cookie = "shared=yes; domain=.site.com"; // termasuk subdomain

// max-age - umur dalam detik
document.cookie = "session=abc; max-age=3600"; // 1 jam

// expires - tanggal expire
document.cookie = "token=xyz; expires=Thu, 01 Jan 2026 00:00:00 GMT";

// secure - hanya via HTTPS
document.cookie = "secret=123; secure";

// samesite - proteksi CSRF
document.cookie = "csrf=token; samesite=strict"; // tidak dikirim cross-site
document.cookie = "pref=dark; samesite=lax";     // dikirim untuk navigasi top-level
document.cookie = "track=id; samesite=none; secure"; // selalu dikirim (harus secure)

// httpOnly - TIDAK bisa diakses JavaScript (hanya dari server)
// ⚠️ Tidak bisa di-set dari JavaScript! Hanya dari HTTP response header
javascript
// Set max-age=0 atau expire di masa lalu
document.cookie = "username=; max-age=0; path=/";
document.cookie = "theme=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";

Helper Functions

javascript
function setCookie(name, value, days = 7) {
  const maxAge = days * 24 * 60 * 60;
  document.cookie = `${name}=${encodeURIComponent(value)}; max-age=${maxAge}; path=/; samesite=lax`;
}

function deleteCookie(name) {
  document.cookie = `${name}=; max-age=0; path=/`;
}

// Penggunaan
setCookie('preferensi', JSON.stringify({ tema: 'dark', bahasa: 'id' }), 30);
const pref = JSON.parse(getCookie('preferensi'));
deleteCookie('preferensi');
⚠️Jebakan!
  1. Ukuran terbatas: Max ~4KB per cookie, ~20 cookie per domain
  2. Dikirim SETIAP request: Cookie besar = lambat! Jangan simpan data besar
  3. document.cookie = ... TIDAK replace semua: Hanya set/update SATU cookie
  4. httpOnly cookie invisible: Tidak muncul di document.cookie (ini fitur keamanan)
  5. Third-party cookies mati: Browser modern blokir cookie dari domain lain

4.2 localStorage & sessionStorage

Apa Itu Web Storage?

localStorage dan sessionStorage adalah cara menyimpan data di browser yang LEBIH BAIK dari cookie untuk data client-side.

Analogi:

  • localStorage = lemari di rumah — data tetap ada walau kamu pergi dan balik lagi (tutup browser, buka lagi)
  • sessionStorage = loker di gym — data hilang begitu kamu pulang (tutup tab)
FiturCookielocalStoragesessionStorage
Ukuran~4KB~5-10MB~5-10MB
Dikirim ke serverYa (otomatis!)TidakTidak
ExpireBisa diaturTidak pernahSaat tab ditutup
AksesServer + ClientClient onlyClient only

localStorage — Data Permanen

javascript
// Simpan
localStorage.setItem('nama', 'Yazid');
localStorage.setItem('umur', '25');

// Simpan object (harus JSON.stringify!)
const user = { nama: 'Yazid', hobi: ['coding', 'gaming'] };
localStorage.setItem('user', JSON.stringify(user));

// Baca
const nama = localStorage.getItem('nama'); // "Yazid"
const userObj = JSON.parse(localStorage.getItem('user'));
console.log(userObj.hobi[0]); // "coding"

// Hapus satu
localStorage.removeItem('umur');

// Hapus semua
localStorage.clear();

// Cek jumlah item
console.log(localStorage.length); // 2

// Akses by index
const key = localStorage.key(0); // nama key pertama

sessionStorage — Data Sementara

javascript
// API sama persis dengan localStorage
sessionStorage.setItem('step', '3');
sessionStorage.getItem('step'); // "3"

// Tapi hilang saat tab ditutup!
// Berguna untuk: wizard/form multi-step, data sementara

Contoh: Tema Dark/Light yang Persisten

javascript
// Simpan preferensi tema
function setTheme(theme) {
  document.body.className = theme;
  localStorage.setItem('theme', theme);
}

// Load tema saat halaman dibuka
function loadTheme() {
  const saved = localStorage.getItem('theme') || 'light';
  document.body.className = saved;
  return saved;
}

// Toggle
let currentTheme = loadTheme();
themeButton.onclick = () => {
  currentTheme = currentTheme === 'light' ? 'dark' : 'light';
  setTheme(currentTheme);
};

Contoh: Form Auto-Save (Draft)

javascript
const form = document.getElementById('postForm');
const DRAFT_KEY = 'post_draft';

// Auto-save setiap ketik
form.addEventListener('input', () => {
  const draft = {
    title: form.title.value,
    content: form.content.value,
    savedAt: Date.now()
  };
  localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
});

// Restore draft saat buka halaman
window.addEventListener('load', () => {
  const draft = JSON.parse(localStorage.getItem(DRAFT_KEY));
  if (draft) {
    const ago = Math.round((Date.now() - draft.savedAt) / 60000);
    if (confirm(`Ada draft dari ${ago} menit lalu. Mau restore?`)) {
      form.title.value = draft.title;
      form.content.value = draft.content;
    }
  }
});

// Hapus draft setelah submit
form.addEventListener('submit', () => {
  localStorage.removeItem(DRAFT_KEY);
});

Storage Event (Sinkronisasi Antar Tab)

javascript
// Event ini HANYA fire di tab LAIN (bukan tab yang mengubah)
window.addEventListener('storage', (event) => {
  console.log(`Key "${event.key}" berubah`);
  console.log(`Dari: ${event.oldValue}`);
  console.log(`Ke: ${event.newValue}`);
  console.log(`URL: ${event.url}`);

  // Contoh: sync login state antar tab
  if (event.key === 'isLoggedIn' && event.newValue === 'false') {
    // User logout di tab lain → redirect ke login
    window.location.href = '/login';
  }
});
⚠️Jebakan!
  1. Hanya simpan STRING: localStorage.setItem('num', 42) → disimpan sebagai "42"
javascript
// ❌ SALAH
localStorage.setItem('arr', [1,2,3]); // disimpan sebagai "1,2,3" (string!)

// ✅ BENAR
localStorage.setItem('arr', JSON.stringify([1,2,3]));
  1. Synchronous & blocking: Operasi besar bisa freeze UI
  2. Tidak aman untuk data sensitif: Bisa dibaca dari DevTools
  3. Quota exceeded: Bisa throw error kalau penuh
javascript
try {
  localStorage.setItem('big', data);
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    alert('Storage penuh! Hapus data lama.');
  }
}

4.3 IndexedDB

Apa Itu IndexedDB?

IndexedDB adalah database di browser — bisa simpan data BESAR dan TERSTRUKTUR. Jauh lebih powerful dari localStorage.

Analogi: Kalau localStorage itu notes di HP (teks sederhana), IndexedDB itu spreadsheet/database lengkap — bisa simpan ribuan data, search, filter, dan index.

Kapan Pakai IndexedDB?

  • Data besar (> 5MB)
  • Perlu search/query
  • Simpan file/blob
  • Offline-first app
  • Cache data API

Membuka Database

javascript
// Buka (atau buat) database
const request = indexedDB.open('MyApp', 1); // nama, versi

// Event: pertama kali buat / upgrade versi
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Buat "tabel" (object store)
  if (!db.objectStoreNames.contains('products')) {
    const store = db.createObjectStore('products', { keyPath: 'id', autoIncrement: true });

    // Buat index untuk search
    store.createIndex('nama', 'nama', { unique: false });
    store.createIndex('kategori', 'kategori', { unique: false });
    store.createIndex('harga', 'harga', { unique: false });
  }
};

// Event: berhasil buka
request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('Database terbuka!', db.name, 'v' + db.version);
};

// Event: error
request.onerror = (event) => {
  console.error('Gagal buka database:', event.target.error);
};

Promise Wrapper (Biar Lebih Enak)

javascript
function openDB(name, version, onUpgrade) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(name, version);
    request.onupgradeneeded = (e) => onUpgrade(e.target.result, e);
    request.onsuccess = (e) => resolve(e.target.result);
    request.onerror = (e) => reject(e.target.error);
  });
}

// Penggunaan
const db = await openDB('MyApp', 1, (db) => {
  db.createObjectStore('products', { keyPath: 'id', autoIncrement: true });
});

CRUD Operations

javascript
// === CREATE (Tambah Data) ===
function addProduct(db, product) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readwrite');
    const store = tx.objectStore('products');
    const request = store.add(product);

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

await addProduct(db, { nama: 'Laptop', kategori: 'elektronik', harga: 15000000 });
await addProduct(db, { nama: 'Buku JS', kategori: 'buku', harga: 150000 });
await addProduct(db, { nama: 'Mouse', kategori: 'elektronik', harga: 250000 });
javascript
// === READ (Baca Data) ===
function getProduct(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readonly');
    const store = tx.objectStore('products');
    const request = store.get(id);

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

const product = await getProduct(db, 1);
console.log(product); // { id: 1, nama: "Laptop", kategori: "elektronik", harga: 15000000 }
javascript
// === READ ALL ===
function getAllProducts(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readonly');
    const store = tx.objectStore('products');
    const request = store.getAll();

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

const allProducts = await getAllProducts(db);
console.log(allProducts); // [{...}, {...}, {...}]
javascript
// === UPDATE ===
function updateProduct(db, product) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readwrite');
    const store = tx.objectStore('products');
    const request = store.put(product); // put = update or insert

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

await updateProduct(db, { id: 1, nama: 'Laptop Gaming', kategori: 'elektronik', harga: 20000000 });
javascript
// === DELETE ===
function deleteProduct(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readwrite');
    const store = tx.objectStore('products');
    const request = store.delete(id);

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

await deleteProduct(db, 2);

Search dengan Index

javascript
// Cari berdasarkan kategori
function getByKategori(db, kategori) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readonly');
    const store = tx.objectStore('products');
    const index = store.index('kategori');
    const request = index.getAll(kategori);

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

const elektronik = await getByKategori(db, 'elektronik');
// [{ nama: "Laptop Gaming", ... }, { nama: "Mouse", ... }]

Cursor (untuk data besar / filter kompleks)

javascript
// Cari produk dengan harga 100rb - 1jt
function getByPriceRange(db, min, max) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('products', 'readonly');
    const store = tx.objectStore('products');
    const index = store.index('harga');
    const range = IDBKeyRange.bound(min, max);
    const results = [];

    const request = index.openCursor(range);
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        results.push(cursor.value);
        cursor.continue(); // lanjut ke item berikutnya
      } else {
        resolve(results); // selesai
      }
    };
    request.onerror = () => reject(request.error);
  });
}

const affordable = await getByPriceRange(db, 100000, 1000000);

Contoh: Offline-First Todo App

javascript
class TodoDB {
  constructor() {
    this.dbPromise = openDB('TodoApp', 1, (db) => {
      const store = db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
      store.createIndex('status', 'status');
      store.createIndex('createdAt', 'createdAt');
    });
  }

  async add(text) {
    const db = await this.dbPromise;
    const todo = { text, status: 'pending', createdAt: Date.now() };
    const tx = db.transaction('todos', 'readwrite');
    const id = await new Promise((resolve, reject) => {
      const req = tx.objectStore('todos').add(todo);
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
    return { ...todo, id };
  }

  async toggle(id) {
    const db = await this.dbPromise;
    const tx = db.transaction('todos', 'readwrite');
    const store = tx.objectStore('todos');

    return new Promise((resolve, reject) => {
      const getReq = store.get(id);
      getReq.onsuccess = () => {
        const todo = getReq.result;
        todo.status = todo.status === 'pending' ? 'done' : 'pending';
        const putReq = store.put(todo);
        putReq.onsuccess = () => resolve(todo);
        putReq.onerror = () => reject(putReq.error);
      };
    });
  }

  async getAll() {
    const db = await this.dbPromise;
    return new Promise((resolve, reject) => {
      const req = db.transaction('todos').objectStore('todos').getAll();
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }
}

// Penggunaan
const todoDB = new TodoDB();
await todoDB.add('Belajar IndexedDB');
await todoDB.add('Buat project');
const todos = await todoDB.getAll();
⚠️Jebakan!
  1. Semua async & event-based: Tidak bisa pakai synchronous seperti localStorage
  2. Versioning ketat: Kalau mau ubah struktur, HARUS naikkan versi database
  3. Transaction auto-commit: Transaction selesai begitu semua request di dalamnya selesai
  4. Tidak bisa query SQL: Tidak ada WHERE harga > 1000 AND kategori = 'buku' — harus pakai cursor + filter manual

🏆 Challenge

Buat "Bookmark Manager" dengan localStorage:

  1. User bisa tambah bookmark (judul + URL)
  2. Tampilkan daftar bookmark
  3. Bisa hapus bookmark
  4. Data persist (tetap ada setelah refresh)
  5. Bonus: Export bookmark sebagai JSON file (pakai Blob + download)
javascript
// Hint:
// - Simpan array of objects di localStorage
// - JSON.stringify saat simpan, JSON.parse saat baca
// - Render ulang list setiap ada perubahan
// - Untuk export: new Blob([json], {type:'application/json'}) + URL.createObjectURL

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.