Hari 19: JavaScript Local Storage – Menyimpan Data di Browser

JavaScript Local Storage - Menyimpan Data di Browser

Bagaimana Website Bisa Mengingat Preferensi Pengguna Meski Browser Ditutup? Hari Ini Kita Akan Mempelajari Local Storage!

Local storage memungkinkan website menyimpan data di browser pengguna. Hari ini kita akan mempelajari cara menyimpan data lokal. Setelah menguasai Fetch API untuk komunikasi dengan server, sekarang saatnya kita pelajari cara menyimpan data langsung di browser – kemampuan krusial untuk membuat aplikasi web yang “mengingat” pengguna!

Keywords: JavaScript local storage, web storage, localStorage, sessionStorage, client-side storage

Mengapa Local Storage Penting dalam Web Development?

Local storage adalah mekanisme penyimpanan data di sisi klien yang memungkinkan aplikasi web menyimpan data tanpa server. Menurut survei W3Techs, 87.5% website menggunakan semacam penyimpanan klien. Dengan local storage, Anda dapat:

  • Menyimpan preferensi pengguna (tema, bahasa, setelan)
  • Membuat aplikasi offline yang fungsional
  • Mengurangi beban server dengan caching data
  • Meningkatkan performa dengan akses data instan
  • Mempertahankan state aplikasi antar sesi
  • Membuat pengalaman pengguna yang personal

Tanpa local storage, aplikasi web akan “lupa” setiap kali browser ditutup, memaksa pengguna mengulang konfigurasi dan kehilangan data yang belum disimpan.

Apa itu Web Storage API?

Web Storage API menyediakan dua mekanisme penyimpanan di browser:

1. localStorage

  • Kapasitas: ~5-10MB per domain
  • Kedaluwarsa: Data bertahan hingga dihapus secara manual
  • Scope: Dapat diakses dari semua window/tab dengan domain yang sama
  • Sinkronisasi: Tersinkron antar tab

2. sessionStorage

  • Kapasitas: ~5MB per domain
  • Kedaluwarsa: Data hilang saat tab/browser ditutup
  • Scope: Hanya dapat diakses dari tab yang membuatnya
  • Sinkronisasi: Tidak tersinkron antar tab

Dasar-dasar Local Storage

Menyimpan Data

// Menyimpan string
localStorage.setItem('username', 'JohnDoe');

// Menggunakan notasi properti
localStorage.userEmail = 'john@example.com';

// Menyimpan objek (harus di-serialize)
const user = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com'
};
localStorage.setItem('user', JSON.stringify(user));

Membaca Data

// Membaca string
const username = localStorage.getItem('username'); // 'JohnDoe'

// Menggunakan notasi properti
const userEmail = localStorage.userEmail; // 'john@example.com'

// Membaca objek (harus di-parse)
const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name); // 'John Doe'

Menghapus Data

// Menghapus item spesifik
localStorage.removeItem('username');

// Menghapus menggunakan notasi properti
delete localStorage.userEmail;

// Menghapus semua data
localStorage.clear();

Mengecek Ketersediaan Storage

function isLocalStorageAvailable() {
  try {
    const testKey = '__test__';
    localStorage.setItem(testKey, testKey);
    localStorage.removeItem(testKey);
    return true;
  } catch (e) {
    return false;
  }
}

console.log('Local storage available:', isLocalStorageAvailable());

Session Storage

Session storage bekerja mirip dengan local storage, tetapi data hanya bertahan selama sesi browser:

// Menyimpan data di session storage
sessionStorage.setItem('sessionID', 'abc123');

// Membaca data
const sessionID = sessionStorage.getItem('sessionID');

// Menghapus data
sessionStorage.removeItem('sessionID');

// Menghapus semua data session storage
sessionStorage.clear();

Event Storage untuk Sinkronisasi Antar Tab

Browser memancarkan event storage saat data berubah di tab lain:

// Menambahkan event listener
window.addEventListener('storage', function(event) {
  console.log('Storage changed!');
  console.log('Key:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('URL:', event.url);

  // Update UI jika perlu
  if (event.key === 'userPreferences') {
    updatePreferences(JSON.parse(event.newValue));
  }
});

Contoh Implementasi: Aplikasi To-Do List dengan Local Storage

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>To-Do List dengan Local Storage</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }

        .container {
            background-color: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }

        .input-container {
            display: flex;
            margin-bottom: 20px;
        }

        #todoInput {
            flex: 1;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 6px 0 0 6px;
            font-size: 16px;
        }

        #addButton {
            padding: 12px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 0 6px 6px 0;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }

        #addButton:hover {
            background-color: #45a049;
        }

        .filter-container {
            margin-bottom: 20px;
            display: flex;
            justify-content: center;
            gap: 10px;
        }

        .filter-btn {
            padding: 8px 16px;
            border: 1px solid #ddd;
            background-color: white;
            border-radius: 20px;
            cursor: pointer;
            transition: all 0.3s;
        }

        .filter-btn.active {
            background-color: #4CAF50;
            color: white;
            border-color: #4CAF50;
        }

        .todo-list {
            list-style: none;
            padding: 0;
        }

        .todo-item {
            display: flex;
            align-items: center;
            padding: 15px;
            border-bottom: 1px solid #eee;
            transition: background-color 0.3s;
        }

        .todo-item:hover {
            background-color: #f9f9f9;
        }

        .todo-item.completed {
            opacity: 0.7;
        }

        .todo-item.completed .todo-text {
            text-decoration: line-through;
            color: #888;
        }

        .todo-checkbox {
            margin-right: 15px;
            transform: scale(1.5);
        }

        .todo-text {
            flex: 1;
            font-size: 16px;
        }

        .todo-actions {
            display: flex;
            gap: 10px;
        }

        .edit-btn, .delete-btn {
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }

        .edit-btn {
            background-color: #2196F3;
            color: white;
        }

        .edit-btn:hover {
            background-color: #0b7dda;
        }

        .delete-btn {
            background-color: #f44336;
            color: white;
        }

        .delete-btn:hover {
            background-color: #d32f2f;
        }

        .stats {
            margin-top: 20px;
            padding: 15px;
            background-color: #f0f0f0;
            border-radius: 6px;
            display: flex;
            justify-content: space-between;
        }

        .clear-completed {
            padding: 8px 16px;
            background-color: #ff9800;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .clear-completed:hover {
            background-color: #e68900;
        }

        .edit-container {
            display: none;
            margin-top: 10px;
        }

        .edit-input {
            flex: 1;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-right: 10px;
        }

        .save-btn, .cancel-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            color: white;
        }

        .save-btn {
            background-color: #4CAF50;
            margin-right: 10px;
        }

        .cancel-btn {
            background-color: #f44336;
        }

        .empty-state {
            text-align: center;
            padding: 40px;
            color: #888;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>To-Do List</h1>

        <div class="input-container">
            <input type="text" id="todoInput" placeholder="Tambah tugas baru...">
            <button id="addButton">Tambah</button>
        </div>

        <div class="filter-container">
            <button class="filter-btn active" data-filter="all">Semua</button>
            <button class="filter-btn" data-filter="active">Aktif</button>
            <button class="filter-btn" data-filter="completed">Selesai</button>
        </div>

        <ul id="todoList" class="todo-list">
            <!-- To-do items akan ditambahkan di sini -->
        </ul>

        <div id="emptyState" class="empty-state" style="display: none;">
            <p>Tidak ada tugas. Tambah tugas baru!</p>
        </div>

        <div class="stats">
            <span id="todoCount">0 tugas tersisa</span>
            <button id="clearCompleted" class="clear-completed">Hapus Selesai</button>
        </div>
    </div>

    <script>
        // Mengakses elemen DOM
        const todoInput = document.getElementById('todoInput');
        const addButton = document.getElementById('addButton');
        const todoList = document.getElementById('todoList');
        const filterBtns = document.querySelectorAll('.filter-btn');
        const todoCount = document.getElementById('todoCount');
        const clearCompletedBtn = document.getElementById('clearCompleted');
        const emptyState = document.getElementById('emptyState');

        // State aplikasi
        let todos = [];
        let currentFilter = 'all';

        // Inisialisasi aplikasi
        function init() {
            // Load todos dari local storage
            loadTodos();

            // Render todos
            renderTodos();

            // Setup event listeners
            setupEventListeners();
        }

        // Load todos dari local storage
        function loadTodos() {
            const storedTodos = localStorage.getItem('todos');
            if (storedTodos) {
                todos = JSON.parse(storedTodos);
            }
        }

        // Save todos ke local storage
        function saveTodos() {
            localStorage.setItem('todos', JSON.stringify(todos));
        }

        // Setup event listeners
        function setupEventListeners() {
            // Add todo
            addButton.addEventListener('click', addTodo);
            todoInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    addTodo();
                }
            });

            // Filter todos
            filterBtns.forEach(btn => {
                btn.addEventListener('click', function() {
                    // Update active filter button
                    filterBtns.forEach(b => b.classList.remove('active'));
                    this.classList.add('active');

                    // Update current filter
                    currentFilter = this.dataset.filter;

                    // Re-render todos
                    renderTodos();
                });
            });

            // Clear completed todos
            clearCompletedBtn.addEventListener('click', clearCompleted);
        }

        // Add new todo
        function addTodo() {
            const text = todoInput.value.trim();

            if (text === '') {
                alert('Masukkan tugas terlebih dahulu!');
                return;
            }

            const newTodo = {
                id: Date.now(),
                text: text,
                completed: false,
                createdAt: new Date().toISOString()
            };

            todos.unshift(newTodo);
            saveTodos();
            renderTodos();

            // Clear input
            todoInput.value = '';
            todoInput.focus();
        }

        // Toggle todo completion
        function toggleTodo(id) {
            todos = todos.map(todo => {
                if (todo.id === id) {
                    return { ...todo, completed: !todo.completed };
                }
                return todo;
            });

            saveTodos();
            renderTodos();
        }

        // Delete todo
        function deleteTodo(id) {
            if (confirm('Apakah Anda yakin ingin menghapus tugas ini?')) {
                todos = todos.filter(todo => todo.id !== id);
                saveTodos();
                renderTodos();
            }
        }

        // Edit todo
        function editTodo(id) {
            const todoItem = document.querySelector(`[data-id="${id}"]`);
            const textElement = todoItem.querySelector('.todo-text');
            const editContainer = todoItem.querySelector('.edit-container');
            const editInput = editContainer.querySelector('.edit-input');

            // Tampilkan edit container
            textElement.style.display = 'none';
            editContainer.style.display = 'flex';

            // Set nilai input
            editInput.value = textElement.textContent;
            editInput.focus();

            // Setup event listeners untuk edit
            const saveBtn = editContainer.querySelector('.save-btn');
            const cancelBtn = editContainer.querySelector('.cancel-btn');

            // Fungsi untuk menyimpan perubahan
            const saveEdit = () => {
                const newText = editInput.value.trim();

                if (newText === '') {
                    alert('Tugas tidak boleh kosong!');
                    return;
                }

                todos = todos.map(todo => {
                    if (todo.id === id) {
                        return { ...todo, text: newText };
                    }
                    return todo;
                });

                saveTodos();
                renderTodos();
            };

            // Event listeners
            saveBtn.addEventListener('click', saveEdit);
            cancelBtn.addEventListener('click', () => {
                renderTodos(); // Batalkan edit
            });

            editInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    saveEdit();
                } else if (e.key === 'Escape') {
                    renderTodos(); // Batalkan edit
                }
            });
        }

        // Clear completed todos
        function clearCompleted() {
            if (confirm('Apakah Anda yakin ingin menghapus semua tugas yang selesai?')) {
                todos = todos.filter(todo => !todo.completed);
                saveTodos();
                renderTodos();
            }
        }

        // Get filtered todos
        function getFilteredTodos() {
            switch (currentFilter) {
                case 'active':
                    return todos.filter(todo => !todo.completed);
                case 'completed':
                    return todos.filter(todo => todo.completed);
                default:
                    return todos;
            }
        }

        // Render todos
        function renderTodos() {
            const filteredTodos = getFilteredTodos();

            // Clear current list
            todoList.innerHTML = '';

            // Show/hide empty state
            if (todos.length === 0) {
                emptyState.style.display = 'block';
                todoList.style.display = 'none';
            } else {
                emptyState.style.display = 'none';
                todoList.style.display = 'block';
            }

            // Render each todo
            filteredTodos.forEach(todo => {
                const li = document.createElement('li');
                li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
                li.setAttribute('data-id', todo.id);

                li.innerHTML = `
                    <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
                    <span class="todo-text">${todo.text}</span>
                    <div class="todo-actions">
                        <button class="edit-btn">Edit</button>
                        <button class="delete-btn">Hapus</button>
                    </div>
                    <div class="edit-container">
                        <input type="text" class="edit-input">
                        <button class="save-btn">Simpan</button>
                        <button class="cancel-btn">Batal</button>
                    </div>
                `;

                // Add event listeners
                const checkbox = li.querySelector('.todo-checkbox');
                const editBtn = li.querySelector('.edit-btn');
                const deleteBtn = li.querySelector('.delete-btn');

                checkbox.addEventListener('change', () => toggleTodo(todo.id));
                editBtn.addEventListener('click', () => editTodo(todo.id));
                deleteBtn.addEventListener('click', () => deleteTodo(todo.id));

                todoList.appendChild(li);
            });

            // Update stats
            updateStats();
        }

        // Update statistics
        function updateStats() {
            const activeTodos = todos.filter(todo => !todo.completed).length;
            todoCount.textContent = `${activeTodos} tugas tersisa`;

            // Show/hide clear completed button
            if (todos.some(todo => todo.completed)) {
                clearCompletedBtn.style.display = 'block';
            } else {
                clearCompletedBtn.style.display = 'none';
            }
        }

        // Initialize app when DOM is loaded
        document.addEventListener('DOMContentLoaded', init);
    </script>
</body>
</html>

Praktik Terbaik dalam Menggunakan Local Storage

1. Selalu Validasi Data

// Saat membaca data dari local storage
function getTodos() {
  try {
    const todos = localStorage.getItem('todos');
    return todos ? JSON.parse(todos) : [];
  } catch (e) {
    console.error('Error parsing todos from localStorage:', e);
    return [];
  }
}

2. Gunakan Namespace untuk Menghindari Konflik

// Gunakan prefix untuk kunci
const APP_PREFIX = 'myApp_';

function setUserData(key, value) {
  localStorage.setItem(`${APP_PREFIX}user_${key}`, JSON.stringify(value));
}

function getUserData(key) {
  const data = localStorage.getItem(`${APP_PREFIX}user_${key}`);
  return data ? JSON.parse(data) : null;
}

3. Batasi Ukuran Data

// Cek kapasitas tersisa
function getRemainingSpace() {
  let data = '';
  for (let i = 0; i < localStorage.length; i++) {
    data += localStorage.getItem(localStorage.key(i));
  }
  return (5 * 1024 * 1024) - unescape(encodeURIComponent(data)).length;
}

// Hapus data lama jika kapasitas hampir penuh
function checkStorageLimit() {
  if (getRemainingSpace() < 100000) { // Kurang dari 100KB
    // Hapus data yang tidak penting
    clearOldData();
  }
}

4. Gunakan Encryption untuk Data Sensitif

// Enkripsi sederhana (untuk demo, gunakan library kripto di produksi)
function simpleEncrypt(text, password) {
  return btoa(text + password);
}

function simpleDecrypt(encryptedText, password) {
  const decoded = atob(encryptedText);
  return decoded.replace(password, '');
}

// Penggunaan
const sensitiveData = 'user_password';
const encrypted = simpleEncrypt(sensitiveData, 'mySecretKey');
localStorage.setItem('secureData', encrypted);

5. Fallback untuk Browser yang Tidak Mendukung

// Fallback ke cookies jika local storage tidak tersedia
function setItem(key, value) {
  if (isLocalStorageAvailable()) {
    localStorage.setItem(key, JSON.stringify(value));
  } else {
    // Fallback ke cookies
    document.cookie = `${key}=${JSON.stringify(value)}; path=/`;
  }
}

function getItem(key) {
  if (isLocalStorageAvailable()) {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : null;
  } else {
    // Fallback ke cookies
    const nameEQ = key + "=";
    const ca = document.cookie.split(';');
    for (let i = 0; i < ca.length; i++) {
      let c = ca[i];
      while (c.charAt(0) === ' ') c = c.substring(1, c.length);
      if (c.indexOf(nameEQ) === 0) {
        return JSON.parse(c.substring(nameEQ.length, c.length));
      }
    }
    return null;
  }
}

6. Handle Storage Events dengan Benar

// Event listener untuk sinkronisasi antar tab
window.addEventListener('storage', function(event) {
  // Abaikan perubahan dari tab ini sendiri
  if (event.storageArea !== localStorage) return;

  // Handle perubahan spesifik
  switch (event.key) {
    case 'userPreferences':
      updatePreferences(JSON.parse(event.newValue));
      break;
    case 'todos':
      updateTodoList(JSON.parse(event.newValue));
      break;
  }
});

Kesimpulan

Local storage adalah kemampuan fundamental yang memungkinkan aplikasi web menyimpan data di browser. Hari ini Anda telah mempelajari:

  • Konsep dasar Web Storage API (localStorage dan sessionStorage)
  • Cara melakukan operasi CRUD (Create, Read, Update, Delete)
  • Event storage untuk sinkronisasi antar tab
  • Implementasi nyata dengan aplikasi to-do list lengkap
  • Praktik terbaik dalam menggunakan local storage

Dengan menguasai local storage, Anda telah membuka kemampuan untuk membuat aplikasi web yang “mengingat” pengguna dan berfungsi tanpa koneksi internet. Konsep ini adalah fondasi untuk membangun Progressive Web Apps (PWA) dan aplikasi offline.

Teruslah berlatih dengan menambahkan local storage ke proyek-proyek Anda. Semakin sering Anda mengimplementasikan penyimpanan klien, semakin alami konsep ini akan terasa dalam pengembangan aplikasi web sehari-hari.

#JavaScript #LocalStorage #WebData #JSIndonesia

Buat aplikasi to-do list yang menyimpan data di local storage dan share screenshotnya! Kami akan memberikan feedback dan tips untuk meningkatkan keterampilan coding Anda. Paling kreatif akan kita highlight di postingan minggu depan dan dapatkan kesempatan untuk ditampilkan di galeri proyek kami! Jangan lupa gunakan hashtag #30HariWebDevChallenge!

Leave a Comment