Bab 4: Penyimpanan Data Browser
4.1 Cookies
Apa Itu Cookie?
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.
Membaca Cookie
// 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"Menulis Cookie
// 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=/`;Opsi Cookie
// 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 headerMenghapus Cookie
// 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
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');- Ukuran terbatas: Max ~4KB per cookie, ~20 cookie per domain
- Dikirim SETIAP request: Cookie besar = lambat! Jangan simpan data besar
document.cookie = ...TIDAK replace semua: Hanya set/update SATU cookie- httpOnly cookie invisible: Tidak muncul di
document.cookie(ini fitur keamanan) - 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)
Perbedaan Cookie vs Web Storage
| Fitur | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| Ukuran | ~4KB | ~5-10MB | ~5-10MB |
| Dikirim ke server | Ya (otomatis!) | Tidak | Tidak |
| Expire | Bisa diatur | Tidak pernah | Saat tab ditutup |
| Akses | Server + Client | Client only | Client only |
localStorage — Data Permanen
// 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 pertamasessionStorage — Data Sementara
// 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 sementaraContoh: Tema Dark/Light yang Persisten
// 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)
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)
// 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';
}
});- Hanya simpan STRING:
localStorage.setItem('num', 42)→ disimpan sebagai "42"
// ❌ SALAH
localStorage.setItem('arr', [1,2,3]); // disimpan sebagai "1,2,3" (string!)
// ✅ BENAR
localStorage.setItem('arr', JSON.stringify([1,2,3]));- Synchronous & blocking: Operasi besar bisa freeze UI
- Tidak aman untuk data sensitif: Bisa dibaca dari DevTools
- Quota exceeded: Bisa throw error kalau penuh
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
// 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)
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
// === 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 });// === 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 }// === 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); // [{...}, {...}, {...}]// === 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 });// === 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
// 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)
// 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
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();- Semua async & event-based: Tidak bisa pakai synchronous seperti localStorage
- Versioning ketat: Kalau mau ubah struktur, HARUS naikkan versi database
- Transaction auto-commit: Transaction selesai begitu semua request di dalamnya selesai
- Tidak bisa query SQL: Tidak ada
WHERE harga > 1000 AND kategori = 'buku'— harus pakai cursor + filter manual
🏆 Challenge
Buat "Bookmark Manager" dengan localStorage:
- User bisa tambah bookmark (judul + URL)
- Tampilkan daftar bookmark
- Bisa hapus bookmark
- Data persist (tetap ada setelah refresh)
- Bonus: Export bookmark sebagai JSON file (pakai Blob + download)
// 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.createObjectURLSudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.