Chapter 10.1☕ 25 min read

Task Manager App — DOM, Events & localStorage

UI sirf State ka dikhaawa hai. Pehle data badlo, phir screen badlo. Yeh hai senior developer ka raaz.

01🏗️ App Architecture: State-Driven UI

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();
}
Why this matters: If your state and DOM drift apart, bugs become impossible to trace. Imagine adding a task to the DOM but forgetting to add it to the array. When the user filters tasks, that task vanishes — it was never in the state! State-first means zero surprises.
02📝 Form Validation & User Input

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");
  }
});
Why e.preventDefault()? The default behavior of a form submit is to reload the page and send data to a server. In a single-page app, we handle everything in JavaScript. e.preventDefault() stops the reload. Without it, your entire app state resets to zero on every submit.
03🔄 Event Delegation: The Efficient Way

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>
How closest() works: 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.
04💾 localStorage: Persisting State

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();
Never trust localStorage data blindly. A user can manually edit it via DevTools. Always validate the shape of the data after parsing it. Does it have the expected properties? Is it an array? A few lines of validation save hours of debugging.
05⚡ DOM Performance: Batching Updates

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 vs DocumentFragment: 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 input and submit events 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 localStorage using JSON.stringify, and load/validate it using JSON.parse wrapped in try/catch.
  • ✅ Batch DOM updates by building a complete HTML string and assigning it to innerHTML once to avoid multiple reflows.
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase