In this tutorial, we’ll build a simple to-do list application using JavaScript, where tasks can be added, removed, and stored persistently using the browser’s local storage. We’ll also explore how to manage the application’s state effectively as it evolves.
Features of the To-Do App:
Add new tasks.
Mark tasks as completed.
Delete tasks.
Persist tasks in local storage so that they remain available after a page refresh.
1. Setting Up the HTML Structure
Let's begin by creating a simple HTML structure for the to-do list.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="todo-container">
<h1>To-Do List</h1>
<form id="todoForm">
<input type="text" id="todoInput" placeholder="Enter a new task" required>
<button type="submit">Add Task</button>
</form>
<ul id="todoList"></ul>
</div>
<script src="app.js"></script>
</body>
</html>
Explanation:
2. Basic Styling with CSS
Let’s add some basic styles to make the app visually appealing.
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f4f4f4;
margin: 0;
}
.todo-container {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h1 {
text-align: center;
}
input[type="text"] {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
width: 100%;
padding: 10px;
background-color: #28a745;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #218838;
}
ul {
list-style-type: none;
padding: 0;
margin-top: 20px;
}
li {
background-color: #f8f9fa;
padding: 10px;
margin-bottom: 5px;
border: 1px solid #ddd;
display: flex;
align-items: center;
border-radius: 3px;
overflow: hidden;
}
li.completed {
text-decoration: line-through;
color: #888;
}
li span {
flex-grow: 1;
margin-right: 10px;
word-wrap: break-word;
}
li button {
background-color: #dc3545;
border: none;
color: white;
padding: 5px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 0.8em;
}
li button:hover {
background-color: #c82333;
}
Explanation:
The styles make the to-do app look clean and user-friendly.
A hover effect is added to the button, and completed tasks will appear with a strikethrough.
3. Writing JavaScript for Task Management
Now, let’s implement the core functionality of the app in app.js.
document.addEventListener("DOMContentLoaded", () => {
const todoForm = document.getElementById("todoForm");
const todoInput = document.getElementById("todoInput");
const todoList = document.getElementById("todoList");
let todos = JSON.parse(localStorage.getItem("todos")) || [];
renderTodos();
todoForm.addEventListener("submit", (e) => {
e.preventDefault();
const newTodo = todoInput.value.trim();
if (newTodo === "") return;
todos.push({ text: newTodo, completed: false });
updateLocalStorage();
renderTodos();
todoInput.value = "";
});
todoList.addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
const index = e.target.parentElement.getAttribute("data-index");
todos.splice(index, 1);
updateLocalStorage();
renderTodos();
}
if (e.target.tagName === "LI") {
const index = e.target.getAttribute("data-index");
todos[index].completed = !todos[index].completed;
updateLocalStorage();
renderTodos();
}
});
function renderTodos() {
todoList.innerHTML = "";
todos.forEach((todo, index) => {
const li = document.createElement("li");
const span = document.createElement("span");
span.textContent = todo.text;
li.setAttribute("data-index", index);
if (todo.completed) {
li.classList.add("completed");
}
li.appendChild(span);
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "Delete";
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
function updateLocalStorage() {
localStorage.setItem("todos", JSON.stringify(todos));
}
});
let todos = JSON.parse(localStorage.getItem("todos")) || [];
Explanation:
State Management: The todos array holds the list of tasks, each task being an object with text and completed properties.
Event Listeners:
Submitting the form adds a new task to the list and updates the state.
Clicking a task marks it as complete, while clicking the "Delete" button removes it.
Rendering: The renderTodos function updates the UI by creating <li> elements for each task, applying a strikethrough if the task is completed.
Local Storage: The tasks are saved in localStorage to persist between sessions.
4. Using Local Storage for Persistence
The tasks are stored in the browser’s local storage as JSON strings. Whenever the tasks are updated, the new state is saved in localStorage.
function updateLocalStorage() {
localStorage.setItem('todos', JSON.stringify(todos));
}
Whenever the page loads or refreshes, the tasks are retrieved from local storage:
let todos = JSON.parse(localStorage.getItem('todos')) || [];
This ensures that the tasks remain on the page, even after a page refresh.
5. Managing State in the To-Do App
In this app, the state is managed using the todos array, which is updated whenever a new task is added, completed, or deleted. Here's how we handle different state changes:
Adding a task:
The task is pushed into the todos array.
Local storage is updated with the new state.
The list is re-rendered to reflect the change.
Toggling completion:
Deleting a task:
The task is removed from the todos array using the splice() method.
Local storage is updated, and the list is re-rendered.
This simple state management strategy ensures the app is reactive and that changes in the state are immediately reflected in the UI

.
Conclusion
Congratulations! You’ve built a functional to-do list app using JavaScript that handles state management and local storage. This app allows users to add, complete, and delete tasks, all while ensuring the data persists across browser sessions.
Key Concepts Covered:
Building interactive forms with JavaScript.
State management using an array of objects.
Using local storage for persisting data across page refreshes.
Enhancing user experience with task completion and deletion.
With this foundation, you can now enhance the app by adding more features like editing tasks, filtering by completed tasks, or adding due dates.