Cara Membuat Todo List Offline dengan IndexDB di JavaScript
Bangun Todo List offline yang kuat dengan IndexDB. Pelajari cara memanfaatkan penyimpanan data lokal browser yang efisien untuk aplikasi web responsif.
@Harry

Membangun Aplikasi Todo List Offline dengan IndexedDB: Solusi Penyimpanan Data Browser yang Powerfull
Aplikasi "Todo List" adalah salah satu proyek klasik bagi pengembang web untuk belajar dasar-dasar interaksi pengguna dan manajemen data. Namun, bagaimana jika kita ingin aplikasi Todo List kita tetap berfungsi bahkan saat pengguna tidak memiliki koneksi internet? Di sinilah IndexedDB hadir sebagai pahlawan.
Artikel ini akan memandu Anda memahami mengapa IndexedDB adalah pilihan yang tepat untuk penyimpanan data sisi klien yang persisten, dan bagaimana kita dapat menggunakannya untuk membangun aplikasi Todo List yang robust dan offline-first.
Pendahuluan: Mengapa Membutuhkan Penyimpanan Data di Browser?
Sebagian besar aplikasi web modern memerlukan cara untuk menyimpan data. Untuk aplikasi Todo List sederhana, ini berarti menyimpan daftar tugas yang dibuat pengguna. Secara default, jika Anda merefresh halaman, semua data yang tidak disimpan akan hilang.
Ada beberapa opsi penyimpanan data di sisi klien (browser):
localStorage/sessionStorage: Mudah digunakan, tetapi terbatas pada string dan kapasitas kecil (sekitar 5-10 MB). Tidak cocok untuk data terstruktur atau volume besar.- Cookies: Terutama untuk manajemen sesi, kapasitas sangat kecil, dan setiap request HTTP akan membawa cookie.
- IndexedDB: Ini adalah API database yang powerful di dalam browser. Dirancang untuk menyimpan data terstruktur dalam jumlah besar (puluhan atau bahkan ratusan MB, tergantung browser), mendukung indeks, transaksi, dan operasi asinkron.
Mengapa Memilih IndexedDB untuk Todo List?
Untuk aplikasi Todo List yang memiliki fitur seperti "simpan offline", "sinkronisasi nanti", atau sekadar menyimpan daftar yang banyak dan kompleks, IndexedDB menawarkan keuntungan signifikan:
- Penyimpanan Data Terstruktur: Anda bisa menyimpan objek JavaScript langsung, tidak hanya string. Ini sangat ideal untuk objek
Todoyang memiliki properti sepertiid,text,completed,dueDate, dll. - Kapasitas Besar: Jauh lebih besar dari
localStorage. - Asinkron: Operasi database tidak akan memblokir thread utama browser, menjaga aplikasi tetap responsif.
- Transaksional: Memastikan integritas data. Operasi adalah "all or nothing", artinya jika ada bagian dari transaksi gagal, seluruh transaksi dibatalkan.
- Indeks: Memungkinkan pencarian data yang efisien berdasarkan properti tertentu (misalnya, mencari semua tugas yang belum selesai).
- Dukungan PWA (Progressive Web Apps): IndexedDB adalah komponen kunci dalam membangun aplikasi web yang dapat bekerja secara offline dan memberikan pengalaman seperti aplikasi native.
Konsep Dasar IndexedDB
Sebelum melangkah ke kode, mari pahami beberapa konsep inti IndexedDB:
- Database: Kontainer utama untuk semua data Anda. Setiap aplikasi biasanya memiliki satu database.
- Versi: Setiap database memiliki versi. Ketika Anda ingin mengubah skema database (misalnya, menambah atau menghapus "object store" atau "index"), Anda harus menaikkan versi database.
- Object Store: Mirip dengan "tabel" dalam database relasional. Ini adalah tempat Anda menyimpan "record" data.
- Record: Setiap item data yang disimpan dalam object store (misalnya, satu objek
todo). - Key Path: Properti dari setiap record yang akan digunakan sebagai kunci unik untuk mengidentifikasi record tersebut (misalnya,
iddari objektodo). - Index: Mekanisme untuk mencari record secara efisien berdasarkan properti selain key path.
- Transaction: Operasi yang dilakukan pada object store harus dilakukan dalam sebuah transaksi. Transaksi bisa
readonly(membaca data) ataureadwrite(membaca dan menulis data). - Request: Setiap operasi IndexedDB mengembalikan objek
IDBRequest. Anda melampirkan event handler (onsuccess,onerror) ke objek ini untuk menangani hasil operasi asinkron.
Membangun Todo List Sederhana dengan IndexedDB
Mari kita mulai dengan struktur dasar HTML dan kemudian fokus pada logika JavaScript untuk berinteraksi dengan IndexedDB.
1. Struktur HTML (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List IndexedDB</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 20px auto; padding: 0 20px; }
input[type="text"] { width: calc(100% - 80px); padding: 8px; margin-right: 5px; }
button { padding: 8px 15px; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { background: #f4f4f4; margin-bottom: 5px; padding: 10px; display: flex; justify-content: space-between; align-items: center; }
li button { background: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer; }
li.completed { text-decoration: line-through; color: #888; }
</style>
</head>
<body>
<h1>My Todo List (Offline Ready!)</h1>
<div id="todo-form">
<input type="text" id="todo-input" placeholder="Tambahkan tugas baru...">
<button id="add-todo-btn">Tambah</button>
</div>
<ul id="todo-list">
<!-- Todos akan dimuat di sini oleh JavaScript -->
</ul>
<script src="app.js"></script>
</body>
</html>
2. Logika JavaScript (app.js)
Sekarang, mari kita tulis kode JavaScript untuk mengelola IndexedDB dan interaksi UI.
// --- Variabel Global dan Konfigurasi IndexedDB ---
const DB_NAME = 'todoDB';
const DB_VERSION = 1; // Tingkatkan versi ini jika Anda mengubah skema database
const OBJECT_STORE_NAME = 'todos';
let db; // Objek database IndexedDB
// --- Fungsi untuk Membuka Koneksi Database ---
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
// Event handler: database berhasil dibuka
request.onsuccess = (event) => {
db = event.target.result;
console.log('Database berhasil dibuka');
resolve(db);
};
// Event handler: error saat membuka database
request.onerror = (event) => {
console.error('Error saat membuka database:', event.target.errorCode);
reject(event.target.error);
};
// Event handler: skema database perlu diperbarui/dibuat
request.onupgradeneeded = (event) => {
db = event.target.result;
console.log('onupgradeneeded: Membuat atau memperbarui object store...');
// Cek apakah object store sudah ada, jika belum, buat
if (!db.objectStoreNames.contains(OBJECT_STORE_NAME)) {
// Buat object store 'todos' dengan 'id' sebagai keyPath dan autoIncrement true
const objectStore = db.createObjectStore(OBJECT_STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
// Buat indeks jika diperlukan (misal: untuk mencari tugas yang belum selesai)
objectStore.createIndex('completed', 'completed', { unique: false });
console.log('Object store "todos" berhasil dibuat.');
}
};
});
}
// --- Fungsi untuk Menambahkan Todo ke Database ---
function addTodoToDB(todoText) {
return new Promise((resolve, reject) => {
// Buat objek transaksi dengan mode 'readwrite'
const transaction = db.transaction([OBJECT_STORE_NAME], 'readwrite');
const objectStore = transaction.objectStore(OBJECT_STORE_NAME);
const newTodo = {
text: todoText,
completed: false,
timestamp: new Date().toISOString()
};
const request = objectStore.add(newTodo);
request.onsuccess = () => {
console.log('Todo berhasil ditambahkan:', newTodo);
resolve(newTodo);
};
request.onerror = (event) => {
console.error('Error saat menambahkan todo:', event.target.error);
reject(event.target.error);
};
transaction.oncomplete = () => {
console.log('Transaksi penambahan todo selesai.');
displayTodos(); // Refresh daftar todo setelah penambahan
};
transaction.onerror = (event) => {
console.error('Transaksi penambahan todo gagal:', event.target.error);
};
});
}
// --- Fungsi untuk Menghapus Todo dari Database ---
function deleteTodoFromDB(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([OBJECT_STORE_NAME], 'readwrite');
const objectStore = transaction.objectStore(OBJECT_STORE_NAME);
const request = objectStore.delete(id);
request.onsuccess = () => {
console.log('Todo berhasil dihapus, ID:', id);
resolve();
};
request.onerror = (event) => {
console.error('Error saat menghapus todo:', event.target.error);
reject(event.target.error);
};
transaction.oncomplete = () => {
console.log('Transaksi penghapusan todo selesai.');
displayTodos(); // Refresh daftar todo setelah penghapusan
};
transaction.onerror = (event) => {
console.error('Transaksi penghapusan todo gagal:', event.target.error);
};
});
}
// --- Fungsi untuk Memperbarui Status Selesai Todo ---
function toggleTodoStatusInDB(id, completedStatus) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([OBJECT_STORE_NAME], 'readwrite');
const objectStore = transaction.objectStore(OBJECT_STORE_NAME);
const getRequest = objectStore.get(id);
getRequest.onsuccess = (event) => {
const todo = event.target.result;
if (todo) {
todo.completed = completedStatus; // Update status
const putRequest = objectStore.put(todo); // Simpan kembali objek yang diperbarui
putRequest.onsuccess = () => {
console.log('Todo status berhasil diperbarui, ID:', id);
resolve();
};
putRequest.onerror = (e) => {
console.error('Error saat memperbarui status todo:', e.target.error);
reject(e.target.error);
};
} else {
console.warn('Todo dengan ID tersebut tidak ditemukan:', id);
reject(new Error('Todo not found'));
}
};
getRequest.onerror = (event) => {
console.error('Error saat mengambil todo untuk update:', event.target.error);
reject(event.target.error);
};
transaction.oncomplete = () => {
console.log('Transaksi pembaruan status todo selesai.');
displayTodos(); // Refresh daftar todo setelah pembaruan
};
transaction.onerror = (event) => {
console.error('Transaksi pembaruan status todo gagal:', event.target.error);
};
});
}
// --- Fungsi untuk Menampilkan Semua Todo dari Database ke UI ---
function displayTodos() {
const todoListElement = document.getElementById('todo-list');
todoListElement.innerHTML = ''; // Kosongkan daftar sebelum mengisi ulang
const transaction = db.transaction([OBJECT_STORE_NAME], 'readonly');
const objectStore = transaction.objectStore(OBJECT_STORE_NAME);
// Mengambil semua objek menggunakan cursor
const request = objectStore.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const todo = cursor.value;
const li = document.createElement('li');
li.dataset.id = todo.id; // Simpan ID di elemen DOM
li.className = todo.completed ? 'completed' : '';
// Checkbox untuk mengubah status completed
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = todo.completed;
checkbox.onchange = (e) => toggleTodoStatusInDB(todo.id, e.target.checked);
li.appendChild(checkbox);
const span = document.createElement('span');
span.textContent = todo.text;
span.style.marginLeft = '10px';
li.appendChild(span);
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Hapus';
deleteButton.onclick = () => deleteTodoFromDB(todo.id);
li.appendChild(deleteButton);
todoListElement.appendChild(li);
cursor.continue(); // Lanjutkan ke record berikutnya
} else {
console.log('Semua todo berhasil ditampilkan.');
}
};
request.onerror = (event) => {
console.error('Error saat menampilkan todo:', event.target.error);
};
}
// --- Event Listener dan Inisialisasi ---
document.addEventListener('DOMContentLoaded', async () => {
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');
// Buka database saat DOMContentLoaded
try {
await openDatabase();
displayTodos(); // Tampilkan todo yang ada setelah database terbuka
} catch (error) {
console.error('Gagal menginisialisasi database:', error);
}
// Event listener untuk tombol 'Tambah'
addTodoBtn.addEventListener('click', async () => {
const todoText = todoInput.value.trim();
if (todoText) {
await addTodoToDB(todoText);
todoInput.value = ''; // Kosongkan input setelah ditambah
}
});
// Event listener untuk tombol 'Enter' pada input
todoInput.addEventListener('keypress', async (event) => {
if (event.key === 'Enter') {
const todoText = todoInput.value.trim();
if (todoText) {
await addTodoToDB(todoText);
todoInput.value = '';
}
}
});
});
Penjelasan Kode
- Variabel Global:
DB_NAME,DB_VERSION, danOBJECT_STORE_NAMEmendefinisikan nama database dan object store kita.dbakan menyimpan referensi ke objek database yang terbuka. openDatabase():- Menggunakan
indexedDB.open(DB_NAME, DB_VERSION)untuk mencoba membuka database. request.onsuccess: Dipanggil jika database berhasil dibuka atau ada. Objekdbakan diisi.request.onerror: Dipanggil jika ada masalah saat membuka database.request.onupgradeneeded: Ini adalah event yang sangat penting. Dipanggil saat database dibuat untuk pertama kalinya, atau saat Anda menaikkanDB_VERSIONdan skema database perlu diubah. Di sinilah kita membuatobjectStore(todos) dengankeyPathiddan agar ID otomatis dibuat. Kita juga membuat indeks pada properti .
- Menggunakan
Kelebihan dan Kekurangan IndexedDB
Kelebihan:
- Offline-first: Sempurna untuk aplikasi yang harus bekerja tanpa internet.
- Kapasitas Besar: Mampu menyimpan data dalam jumlah yang sangat besar.
- Data Terstruktur: Menyimpan objek JavaScript langsung, sangat fleksibel.
- Performa: Cepat untuk operasi lokal karena data disimpan di perangkat pengguna.
- Robust: Desain transaksional memastikan integritas data.
Kekurangan:
- API yang Kompleks: Dibandingkan
localStorage, API IndexedDB lebih verbose dan memerlukan pemahaman tentang konsep asinkron, transaksi, dan event handler. - Debugging: Meskipun browser modern memiliki alat pengembang yang baik untuk IndexedDB (misalnya, tab "Application" di Chrome), debugging bisa sedikit lebih menantang.
- Tidak Ada Query Bahasa seperti SQL: Anda harus menggunakan
cursoratauindexuntuk melakukan pencarian. - Tidak Bisa Diakses Langsung dari Web Worker Biasa: Hanya dari Dedicated Worker, Shared Worker, atau Service Worker.
Kesimpulan
IndexedDB adalah teknologi yang sangat powerful untuk membangun aplikasi web yang kaya fitur dan offline-first. Meskipun API-nya mungkin terlihat rumit pada pandangan pertama, memahami konsep dasarnya akan membuka pintu bagi Anda untuk membuat aplikasi web yang lebih tangguh dan memberikan pengalaman pengguna yang lebih baik.
Dengan aplikasi Todo List sederhana ini, Anda telah melihat bagaimana IndexedDB dapat digunakan untuk menyimpan, mengambil, memperbarui, dan menghapus data secara persisten di browser. Ini adalah fondasi yang bagus untuk proyek PWA Anda selanjutnya!
Selamat mencoba dan eksplorasi lebih lanjut!