Learning overview
- Estimated time
- 4 minutes
- Difficulty
- Beginner
- Prerequisites
- No prior experience required
- Learning outcome
- Explain javascript using a clear mental model · Apply javascript in practical JavaScript work
- Last updated
- July 18, 2026
What you will build and learn
The application can add, complete, filter, and remove tasks. State lives in one array of objects. Every user action changes state and then calls one render function, keeping data logic separate from DOM output.
- Represent application state with arrays and objects
- Update state without hidden mutation
- Render with safe DOM APIs
- Persist and restore JSON data
Step 1: Define the state model
Each task needs a stable ID, text, and completion flag. Const prevents the array binding from being reassigned accidentally; replace the binding with let only if your update strategy assigns a new array.
let tasks = [
{ id: crypto.randomUUID(), text: "Review array methods", completed: false },
];
let activeFilter = "all";Step 2: Write data transformations
Keep state operations independent from the DOM. Map returns an array with one task changed, filter removes a matching task, and another filter derives the visible set.
function toggleTask(items, id) {
return items.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
);
}
function removeTask(items, id) {
return items.filter((task) => task.id !== id);
}
function visibleTasks(items, filter) {
if (filter === "active") return items.filter((task) => !task.completed);
if (filter === "completed") return items.filter((task) => task.completed);
return items;
}Step 3: Render state safely
Clear the list, create elements, set text through textContent, and attach task IDs through dataset. This avoids parsing user text as HTML and gives event handlers a stable identifier.
const list = document.querySelector("#task-list");
function render() {
list.replaceChildren();
for (const task of visibleTasks(tasks, activeFilter)) {
const item = document.createElement("li");
item.dataset.taskId = task.id;
const button = document.createElement("button");
button.type = "button";
button.textContent = task.completed ? `Undo ${task.text}` : `Complete ${task.text}`;
button.dataset.action = "toggle";
item.append(button);
list.append(item);
}
}Step 4: Connect user events
The form handler validates trimmed input and creates a task. Event delegation on the list handles buttons added during future renders without registering a separate listener for every item.
document.querySelector("#task-form").addEventListener("submit", (event) => {
event.preventDefault();
const input = event.currentTarget.elements.task;
const text = input.value.trim();
if (!text) return;
tasks = [...tasks, { id: crypto.randomUUID(), text, completed: false }];
input.value = "";
save();
render();
});
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button) return;
tasks = toggleTask(tasks, button.closest("li").dataset.taskId);
save();
render();
});Step 5: Persist and restore tasks
Local storage stores strings, so serialize the array as JSON. Parsing can fail when stored data is corrupted; recover to an empty list instead of preventing the application from starting.
const storageKey = "avora.tasks.v1";
function save() {
localStorage.setItem(storageKey, JSON.stringify(tasks));
}
function load() {
try {
const stored = JSON.parse(localStorage.getItem(storageKey) ?? "[]");
tasks = Array.isArray(stored) ? stored : [];
} catch {
tasks = [];
}
}
load();
render();Step 6: Test behavior and edge cases
Test data functions independently, then test the complete user flow. Add an empty state, accessible status message, delete action, and clear-completed action only after the core behavior is reliable.
- Whitespace-only tasks are rejected
- Repeated task text still receives distinct IDs
- Filters update after completion changes
- Special characters render as text
- Corrupt saved data does not crash startup
- All actions work using only a keyboard
Frequently asked questions
Why keep JavaScript state separate from the DOM?
A separate state model makes behavior predictable, testable, and easier to persist. The DOM becomes an output of state instead of a second competing data store.
Why use textContent instead of innerHTML?
textContent renders user input as text. innerHTML parses markup and can create cross-site scripting vulnerabilities when data is not strictly trusted and sanitized.
Is local storage suitable for sensitive data?
No. It is readable by scripts running on the origin and has no built-in expiration. Use it only for non-sensitive client-side preferences or small application data.
Topic graph
A taxonomy-generated path through this subject.
Related courses
Related learning
Recommended automatically from shared technologies, topics, intent, and difficulty.
Related Guides
Related Tutorials
Related Glossary
Related Cheatsheets
Related Projects
Related Interview Questions
Related reading
Hand-picked companion pages that deepen this topic.
Your next steps
