Task Manager App — DOM, Events & localStorage
UI sirf State ka dikhaawa hai. Pehle data badlo, phir screen badlo. Yeh hai senior developer ka raaz.
The Golden Rule of UI: UI is a function of State. UI = f(state). The screen you see is just a reflection of your data. Change the data, the screen updates.
We maintain a single tasks array — our Single Source of Truth. Every change updates this array first, then the DOM re-renders based on the new state.
Never manipulate DOM directly based on events. Follow the flow: Event → Update State → Re-render.
This unidirectional data flow makes your app predictable and debuggable. When something goes wrong, you always know where to look — the state.
// The Single Source of Truth
let tasks = [];
let filter = "all"; // "all", "active", "completed"
function addTask(title) {
const newTask = {
id: Date.now(),
title: title,
completed: false
};
tasks.push(newTask);
saveTasks(); // Persist to localStorage
renderTasks(); // Update DOM based on new state
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
renderTasks();
}
Always sanitize and validate user input before adding it to state. A task manager that accepts empty or garbage input is not a task manager — it is a bug factory.
Prevent empty tasks or tasks with only whitespace. Enforce a minimum length. Provide real-time visual feedback so the user knows what is wrong before they hit submit.
Use the input event for real-time validation and the submit event for final validation.
const form = document.getElementById("task-form");
const input = document.getElementById("task-input");
form.addEventListener("submit", (e) => {
e.preventDefault(); // Prevent page reload
const title = input.value.trim();
// Validation
if (!title) {
input.classList.add("error");
showError("Task cannot be empty!");
return;
}
if (title.length < 3) {
showError("Task must be at least 3 characters!");
return;
}
// Valid input -> Update State
addTask(title);
input.value = ""; // Clear input
input.classList.remove("error");
});
// Real-time validation feedback
input.addEventListener("input", () => {
if (input.value.trim().length >= 3) {
input.classList.remove("error");
}
});
e.preventDefault() stops the reload. Without it, your entire app state resets to zero on every submit.A task manager has dynamic elements — delete buttons, checkboxes, edit triggers. Adding individual event listeners to each element is inefficient and breaks when new tasks are added.
Solution: Event Delegation. Attach ONE listener to the parent container (<ul>). Events from children bubble up to the parent. Use e.target.closest() to find which action was clicked.
Use data-* attributes to store task IDs directly in the DOM element — no need to search for them.
const taskList = document.getElementById("task-list");
taskList.addEventListener("click", (e) => {
const target = e.target;
// Check if delete button was clicked
if (target.closest(".delete-btn")) {
const li = target.closest("li");
const id = Number(li.dataset.id);
deleteTask(id);
}
// Check if checkbox was clicked
if (target.closest(".toggle-check")) {
const li = target.closest("li");
const id = Number(li.dataset.id);
toggleTask(id);
}
});
// Each task item has a data-id attribute
// <li data-id="123">
// <input type="checkbox" class="toggle-check">
// <span>Task title</span>
// <button class="delete-btn">Delete</button>
// </li>
e.target.closest(".delete-btn") starts at the clicked element and walks UP the DOM tree until it finds an element matching the selector. If you click the text inside a button, e.target is the text node — but closest() still finds the button. This is why delegation is bulletproof.localStorage stores key-value pairs as STRINGS only. It does NOT store objects or arrays. Think of it as a persistent Map<string, string>.
To save an array: JSON.stringify(tasks). To read it: JSON.parse(localStorage.getItem("tasks")).
Always wrap JSON.parse in try/catch. If the data in localStorage is corrupted, the app should not crash — it should fall back gracefully.
Save to localStorage on every state change. Load from localStorage on app startup.
const STORAGE_KEY = "devInHyd_tasks";
function saveTasks() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
} catch (e) {
console.error("Could not save to localStorage", e);
// Handle QuotaExceededError if storage is full
}
}
function loadTasks() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
tasks = JSON.parse(stored);
// Validate the parsed data structure!
if (!Array.isArray(tasks)) tasks = [];
}
} catch (e) {
console.error("Corrupted data in localStorage", e);
tasks = []; // Fallback to empty state
}
renderTasks();
}
// Init app
loadTasks();
Calling innerHTML in a loop causes N reflows/repaints. This is extremely slow — the browser must recalculate the layout for every single iteration.
Instead, build the entire HTML string first, then assign it to innerHTML ONCE. One string, one DOM update, one reflow.
innerHTML destroys old DOM nodes and their event listeners. Since we use Delegation on the parent, our listeners survive the re-render.
// SLOW: innerHTML += in a loop
tasks.forEach(t => {
taskList.innerHTML += "<li>" + t.title + "</li>";
// Browser recalculates layout N times!
});
// FAST: Build string, assign once
function renderTasks() {
if (tasks.length === 0) {
taskList.innerHTML = "<li>No tasks yet!</li>";
return;
}
const html = tasks.map(t =>
"<li data-id=" + t.id + ">" + t.title + "</li>"
).join("");
taskList.innerHTML = html; // Single DOM update, single reflow
}
innerHTML is simple but destroys all child nodes and their listeners. DocumentFragment is safer — you append nodes to a fragment (in memory, no reflow), then insert the fragment once. For our task manager with Event Delegation, innerHTML is fine because our listener lives on the parent, not the children.Lo kar liya — Key Points:
- ✅ UI should be a pure function of State (UI = f(state)). Update state first, then re-render.
- ✅ Sanitize and validate all user input on both
inputandsubmitevents before updating state. - ✅ Use Event Delegation on the parent container instead of adding listeners to dynamic child elements.
- ✅ Use
data-*attributes in HTML to pass IDs from the DOM to the event handler. - ✅ Persist state to
localStorageusingJSON.stringify, and load/validate it usingJSON.parsewrapped in try/catch. - ✅ Batch DOM updates by building a complete HTML string and assigning it to
innerHTMLonce to avoid multiple reflows.
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login