Client-Side Storage with IndexedDB

Cloud computing data management technology system for computer network cyber security or database file document transfer and sharing, big data for finance data analytics to growth concepts.

The Challenge / Context

Modern web applications frequently handle structured datasets that exceed the limits of traditional client-side storage mechanisms. While localStorage is convenient for small configuration settings, it operates synchronously on the main thread and caps storage at roughly 5MB. Running heavy read and write operations against localStorage introduces noticeable UI jank and locks the main execution thread during data-heavy interactions.

IndexedDB solves this bottleneck by providing a full, transactional, asynchronous NoSQL database directly inside the browser. It handles significant storage quotas, supports indexes for high-speed lookups, and stores complex objects like Blobs, Files, and nested structures without manual serialization. Mastering native IndexedDB patterns allows you to build resilient, offline-first web apps that maintain high performance even under flaky network conditions.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IndexedDB API Demo</title>
    <link href="style.css" rel="stylesheet" />
    <script src="indexedDB_API.js" defer></script>
</head>
    
<body>
    <main class="app-container">
        <header>
            <h1>Task Storage</h1>
            <p class="subtitle">Powered by Native IndexedDB API</p>
        </header>
    
        <form id="task-form" class="card">
            <div class="form-group">
                <label for="task-title">Task Title</label>
                <input type="text" id="task-title" placeholder="e.g., Cache API payloads" required />
            </div>
    
            <div class="form-group">
                <label for="task-category">Category</label>
                <select id="task-category">
                    <option value="Development">Development</option>
                    <option value="Architecture">Architecture</option>
                    <option value="DevOps">DevOps</option>
                </select>
            </div>
    
            <button type="submit" class="btn primary-btn">Add Task</button>
        </form>
    
        <section class="task-section">
            <div class="section-header">
                <h2>Stored Records</h2>
                <button id="clear-all-btn" class="btn danger-btn-outline">Clear DB</button>
            </div>
            <ul id="task-list" class="task-list" aria-live="polite"></ul>
            <p id="empty-state" class="empty-state">No records found in IndexedDB.</p>
        </section>
    </main>
</body>

</html>
:root {
    --bg: #0f172a;
    --surface: #1e293b;
    --border: #334155;
    --text-main: #f8fafc;
    --text-muted: #94a3b8;
    --primary: #38bdf8;
    --primary-hover: #0ea5e9;
    --danger: #f43f5e;
    --danger-hover: #e11d48;
    --radius: 8px;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background-color: var(--bg);
    color: var(--text-main);
    display: flex;
    justify-content: center;
    padding: 2rem 1rem;
    min-height: 100vh;
}

.app-container {
    width: 100%;
    max-width: 580px;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

header h1 {
    font-size: 1.75rem;
    font-weight: 700;
}

.subtitle {
    color: var(--text-muted);
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

.card {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

label {
    font-size: 0.8rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--text-muted);
}

input,
select {
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text-main);
    padding: 0.65rem 0.75rem;
    border-radius: var(--radius);
    font-size: 0.95rem;
    outline: none;
}

input:focus,
select:focus {
    border-color: var(--primary);
}

.btn {
    cursor: pointer;
    border-radius: var(--radius);
    padding: 0.65rem 1rem;
    font-weight: 600;
    font-size: 0.9rem;
    transition: background-color 0.15s ease;
    border: none;
}

.primary-btn {
    background-color: var(--primary);
    color: #000;
}

.primary-btn:hover {
    background-color: var(--primary-hover);
}

.danger-btn-outline {
    background: transparent;
    color: var(--danger);
    border: 1px solid var(--danger);
    padding: 0.35rem 0.65rem;
    font-size: 0.8rem;
}

.danger-btn-outline:hover {
    background-color: var(--danger);
    color: #fff;
}

.section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.75rem;
}

.section-header h2 {
    font-size: 1.1rem;
}

.task-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.task-item {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.task-info h3 {
    font-size: 0.95rem;
    margin-bottom: 0.2rem;
}

.badge {
    display: inline-block;
    font-size: 0.75rem;
    color: var(--primary);
    background: rgba(56, 189, 248, 0.1);
    padding: 0.15rem 0.5rem;
    border-radius: 4px;
}

.delete-btn {
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 1.25rem;
    cursor: pointer;
}

.delete-btn:hover {
    color: var(--danger);
}

.empty-state {
    text-align: center;
    color: var(--text-muted);
    font-size: 0.9rem;
    padding: 2rem 0;
    display: none;
}
/**
 * IndexedDB Configuration and Safe DOM Controller
 * Built without innerHTML to prevent XSS vulnerabilities.
 */

const DB_NAME = 'TaskManagerDB';
const DB_VERSION = 1;
const STORE_NAME = 'tasks';

// DOM Element References using querySelector
const taskForm = document.querySelector('#task-form');
const taskTitleInput = document.querySelector('#task-title');
const taskCategorySelect = document.querySelector('#task-category');
const taskList = document.querySelector('#task-list');
const emptyState = document.querySelector('#empty-state');
const clearAllBtn = document.querySelector('#clear-all-btn');

/**
 * Opens or upgrades the IndexedDB database instance.
 * @returns {Promise<IDBDatabase>}
 */
function openDB() {
	return new Promise((resolve, reject) => {
		// Feature detection for IndexedDB support
		if (!('indexedDB' in window)) {
			return reject(new Error('IndexedDB is not supported in this browser.'));
		}

		const request = window.indexedDB.open(DB_NAME, DB_VERSION);

		// Schema initialization runs only on version changes
		request.onupgradeneeded = (event) => {
			const db = event.target.result;
			if (!db.objectStoreNames.contains(STORE_NAME)) {
				db.createObjectStore(STORE_NAME, {
					keyPath: 'id',
					autoIncrement: true
				});
			}
		};

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

/**
 * Persists a new task object to the store.
 * @param {Object} task
 * @returns {Promise<number>}
 */
async function addTask(task) {
	const db = await openDB();
	return new Promise((resolve, reject) => {
		const tx = db.transaction(STORE_NAME, 'readwrite');
		const store = tx.objectStore(STORE_NAME);
		const request = store.add(task);

		request.onsuccess = () => resolve(request.result);
		request.onerror = () => reject(request.error);
		tx.oncomplete = () => db.close();
	});
}

/**
 * Fetches all stored tasks via a readonly transaction.
 * @returns {Promise<Array>}
 */
async function getAllTasks() {
	const db = await openDB();
	return new Promise((resolve, reject) => {
		const tx = db.transaction(STORE_NAME, 'readonly');
		const store = tx.objectStore(STORE_NAME);
		const request = store.getAll();

		request.onsuccess = () => resolve(request.result || []);
		request.onerror = () => reject(request.error);
		tx.oncomplete = () => db.close();
	});
}

/**
 * Deletes a task by primary key ID.
 * @param {number} id
 * @returns {Promise<void>}
 */
async function deleteTask(id) {
	const db = await openDB();
	return new Promise((resolve, reject) => {
		const tx = db.transaction(STORE_NAME, 'readwrite');
		const store = tx.objectStore(STORE_NAME);
		const request = store.delete(id);

		request.onsuccess = () => resolve();
		request.onerror = () => reject(request.error);
		tx.oncomplete = () => db.close();
	});
}

/**
 * Clears all records from the object store.
 * @returns {Promise<void>}
 */
async function clearAllTasks() {
	const db = await openDB();
	return new Promise((resolve, reject) => {
		const tx = db.transaction(STORE_NAME, 'readwrite');
		const store = tx.objectStore(STORE_NAME);
		const request = store.clear();

		request.onsuccess = () => resolve();
		request.onerror = () => reject(request.error);
		tx.oncomplete = () => db.close();
	});
}

/**
 * Safely constructs a DOM list item using native document methods.
 * Ensures user data is treated strictly as text, preventing XSS.
 * @param {Object} task
 * @returns {HTMLLIElement}
 */
function createTaskElement(task) {
	const li = document.createElement('li');
	li.className = 'task-item';

	const taskInfo = document.createElement('div');
	taskInfo.className = 'task-info';

	const heading = document.createElement('h3');
	// Use textContent instead of innerHTML to safely escape raw strings
	heading.textContent = task.title;

	const badge = document.createElement('span');
	badge.className = 'badge';
	badge.textContent = task.category;

	taskInfo.appendChild(heading);
	taskInfo.appendChild(badge);

	const deleteBtn = document.createElement('button');
	deleteBtn.className = 'delete-btn';
	deleteBtn.setAttribute('aria-label', `Delete task ${task.title}`);
	deleteBtn.dataset.id = String(task.id);
	deleteBtn.textContent = '×';

	li.appendChild(taskInfo);
	li.appendChild(deleteBtn);

	return li;
}

/**
 * Reads tasks from IndexedDB and replaces the current DOM tree.
 */
async function renderTasks() {
	try {
		const tasks = await getAllTasks();

		// Safely empty the list container
		taskList.replaceChildren();

		if (tasks.length === 0) {
			if (emptyState) emptyState.style.display = 'block';
			return;
		}

		if (emptyState) emptyState.style.display = 'none';

		// Batch append to avoid unnecessary reflows
		const fragment = document.createDocumentFragment();
		tasks.forEach((task) => {
			fragment.appendChild(createTaskElement(task));
		});

		taskList.appendChild(fragment);
	} catch (error) {
		console.error('Failed to render tasks from IndexedDB:', error);
	}
}

// Event Listeners
if (taskForm && taskTitleInput && taskCategorySelect) {
	taskForm.addEventListener('submit', async (e) => {
		e.preventDefault();

		const title = taskTitleInput.value.trim();
		const category = taskCategorySelect.value;

		if (!title) return;

		try {
			await addTask({
				title,
				category,
				createdAt: new Date().toISOString()
			});
			taskForm.reset();
			await renderTasks();
		} catch (error) {
			console.error('Failed to save task:', error);
		}
	});
}

if (taskList) {
	taskList.addEventListener('click', async (e) => {
		const target = e.target;
		// Check if clicked element is the delete button
		if (
			target instanceof HTMLButtonElement &&
			target.classList.contains('delete-btn')
		) {
			const id = Number(target.dataset.id);
			if (!Number.isNaN(id)) {
				try {
					await deleteTask(id);
					await renderTasks();
				} catch (error) {
					console.error(`Failed to delete task ${id}:`, error);
				}
			}
		}
	});
}

if (clearAllBtn) {
	clearAllBtn.addEventListener('click', async () => {
		try {
			await clearAllTasks();
			await renderTasks();
		} catch (error) {
			console.error('Failed to clear object store:', error);
		}
	});
}

// Initial bootstrap on DOM ready
document.addEventListener('DOMContentLoaded', renderTasks);
more posts: