Problem Statement
Build a todo list where the user can add tasks, mark them done and delete them. The list should still be there after a page refresh.
Functional Requirements
- Add: Add a task with the Add Task button or the Enter key. Blank or whitespace only input is ignored, and the input clears after adding.
- Complete: Clicking the task text or the check button toggles
isCompleted, which shows the text struck through. - Delete: The delete button removes that task from the list.
- Persist: Save
todostolocalStorageunder thetodoskey on every change, and read it back on first render.
Technical Constraints
- Each todo is
{ value, isCompleted, id }, withidfromnew Date().getTime(). - State updates are immutable, using
mapandfilterto build new arrays. - Editing is not built.
Itemhas anisEditingstate and an empty edit button, but they do nothing yet.
Code Flow
-
TodoComponent:- The
Todocomponent manages the main todo list functionality. - It uses
useStateto manage the current task input (task) and the list of todos (todos). - The
todosstate is initialized fromlocalStorageif available, otherwise defaults to an empty array. - When todos change,
useEffectautomatically saves them tolocalStorage. - The component handles adding, completing, and deleting todos.
- The
-
ItemComponent:- The
Itemcomponent renders individual todo items. - It receives
todo,handleComplete, andhandleDeleteas props. - It uses
useStateto manage editing state (though editing functionality is not fully implemented). - Each item displays the todo text and action buttons for completing and deleting.
- The
-
AppComponent:- The
Appcomponent is the root component that renders theTodocomponent. - It serves as the entry point for the application.
- The
Key Concepts
1. useState Hook
- Purpose: The
useStatehook is used to manage component-level state in functional components. - How it works:
useState("")initializes thetaskstate to an empty string for the input field.useState(JSON.parse(localStorage.getItem("todos")) || [])initializes thetodosstate by reading fromlocalStorageor defaulting to an empty array.- State updates trigger re-renders, updating the UI to reflect changes.
const [task, setTask] = useState("");
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem("todos")) || []
);2. useEffect Hook
- Purpose: The
useEffecthook is used to perform side effects, such as saving data tolocalStorage. - How it works:
- The effect runs whenever the
todosarray changes (dependency array[todos]). - It saves the todos to
localStorageusingJSON.stringify()to convert the array to a string. - This ensures todos persist across page refreshes.
- The effect runs whenever the
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);3. localStorage API
- Purpose:
localStorageprovides persistent storage in the browser, allowing data to survive page refreshes. - How it works:
localStorage.getItem("todos")retrieves stored todos as a string.JSON.parse()converts the string back to a JavaScript array.localStorage.setItem("todos", JSON.stringify(todos))saves the todos array as a string.- The
|| []fallback ensures we always have an array, even iflocalStorageis empty.
// Reading from localStorage
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem("todos")) || []
);
// Writing to localStorage
localStorage.setItem("todos", JSON.stringify(todos));4. Array Methods
.map() Method
- Purpose: Creates a new array by transforming each element.
- Usage in Todo:
- Used to create a copy of todos before adding a new one (immutability pattern).
- Used to render multiple
Itemcomponents from the todos array.
// Creating a copy for immutability
const newTodo = todos.map((todo) => ({ ...todo }));
// Rendering items
{todos.map((todo) => (
<Item key={todo.id} todo={todo} ... />
))}.filter() Method
- Purpose: Creates a new array containing only elements that pass a test.
- Usage in Todo: Removes a todo by filtering out the one with the matching ID.
const handleDelete = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};5. Event Handling
Input Change Handler
- Purpose: Updates the
taskstate as the user types. - How it works:
onChangeevent fires on every keystroke.e.target.valuecontains the current input value.- Updates state, triggering a re-render with the new value.
const handleChange = (e) => {
setTask(e.target.value);
};Keyboard Event Handler
- Purpose: Allows adding todos by pressing Enter key.
- How it works:
onKeyDownevent fires when a key is pressed.- Checks if the pressed key is "Enter".
- Calls
handleAddTask()if Enter is pressed.
const handleKeyDown = (e) => {
if (e.key === "Enter") {
handleAddTask();
}
};6. Immutability Pattern
- Purpose: React requires state updates to be immutable (creating new objects/arrays instead of mutating existing ones).
- How it works:
- When adding a todo, we create a new array using
.map()to copy existing todos. - When updating a todo, we use
.map()to create a new array with the updated item. - When deleting, we use
.filter()to create a new array without the deleted item.
- When adding a todo, we create a new array using
// Adding a todo (immutable)
const newTodo = todos.map((todo) => ({ ...todo }));
newTodo.push({ value: task, isCompleted: false, id: new Date().getTime() });
setTodos(newTodo);
// Updating a todo (immutable)
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
)
);
// Deleting a todo (immutable)
setTodos(todos.filter((todo) => todo.id !== id));7. Unique ID Generation
- Purpose: Each todo needs a unique identifier for React's
keyprop and for operations like delete/update. - How it works:
- Uses
new Date().getTime()to generate a timestamp-based unique ID. - This ensures each todo has a unique identifier.
- Uses
{
value: task,
isCompleted: false,
id: new Date().getTime(),
}8. Conditional Rendering
- Purpose: Display different content based on state or conditions.
- How it works:
- The todo text receives a conditional class based on
isCompletedstatus. - Empty task validation prevents adding blank todos.
- The todo text receives a conditional class based on
// Conditional class
<span className={`todo-text ${todo?.isCompleted ? "completed" : ""}`}>
// Conditional validation
if (!task.trim()) return; // Prevent empty tasksCode Breakdown
Todo Component
import { useEffect, useState } from "react";
import Item from "./Item.jsx";
function Todo() {
const [task, setTask] = useState("");
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem("todos")) || []
);
const handleChange = (e) => {
setTask(e.target.value);
};
const handleAddTask = () => {
if (!task.trim()) return;
const newTodo = todos.map((todo) => ({ ...todo }));
newTodo.push({
value: task,
isCompleted: false,
id: new Date().getTime(),
});
setTodos(newTodo);
setTask("");
};
const handleKeyDown = (e) => {
if (e.key === "Enter") {
handleAddTask();
}
};
const handleDelete = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const handleComplete = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
)
);
};
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
return (
<div className="todo-container">
<h1 className="todo-title">Todo List</h1>
<div className="input-container">
<input
type="text"
value={task}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Add a new task..."
className="todo-input"
/>
<button onClick={handleAddTask} className="add-button">
Add Task
</button>
</div>
<div className="todos-list">
{todos.map((todo) => {
return (
<Item
key={todo.id}
todo={todo}
handleComplete={handleComplete}
handleDelete={handleDelete}
/>
);
})}
</div>
</div>
);
}
export default Todo;-
State:
task: String state for the input field value.todos: Array state containing all todo items, initialized fromlocalStorage.
-
Functions:
handleChange: Updatestaskstate on input change.handleAddTask: Adds a new todo to the array, validates for empty input, generates unique ID.handleKeyDown: Allows adding todos with Enter key.handleDelete: Removes a todo by filtering it out.handleComplete: Toggles theisCompletedproperty of a todo.
-
Effect:
useEffectsaves todos tolocalStoragewhenever the todos array changes.
-
Rendering:
- Maps over
todosarray to renderItemcomponents. - Each
Itemreceivestodo,handleComplete, andhandleDeleteas props.
- Maps over
Item Component
import { useState } from "react";
function Item({ todo, handleComplete, handleDelete }) {
const [isEditing, setIsEditing] = useState(false);
const handleEdit = (e) => {};
return (
<div className="todo-item">
<span
className={`todo-text ${todo?.isCompleted ? "completed" : ""}`}
onClick={() => handleComplete(todo?.id)}
>
{todo?.value}
</span>
<div className="todo-actions">
<button
className="action-button complete"
onClick={() => handleComplete(todo?.id)}
title="Mark as complete"
>
✓
</button>
<button
className="action-button delete"
onClick={() => handleDelete(todo?.id)}
title="Delete task"
>
×
</button>
{isEditing && <span>✏️</span>}
<button
className="action-button edit"
onClick={() => handleEdit()}
></button>
</div>
</div>
);
}
export default Item;-
Props:
todo: Object containingvalue,isCompleted, andidproperties.handleComplete: Function to toggle completion status.handleDelete: Function to delete the todo.
-
State:
isEditing: Boolean state for editing mode (not fully implemented).
-
Rendering:
- Displays todo text with conditional class based on completion status.
- Provides buttons for completing and deleting todos.
- Uses optional chaining (
todo?.isCompleted) to safely access properties.
App Component
import "./App.css";
import Todo from "./components/Todo.jsx";
function App() {
return (
<>
<Todo />
</>
);
}
export default App;- Purpose: Root component that renders the main
Todocomponent. - Structure: Simple wrapper component that serves as the application entry point.
Summary
-
Code Flow:
- The
Appcomponent renders theTodocomponent. - The
Todocomponent initializestodosfromlocalStorageor defaults to an empty array. - User types a task in the input field, updating the
taskstate. - User clicks "Add Task" or presses Enter, triggering
handleAddTask(). - A new todo object is created with a unique ID and added to the
todosarray. - The
useEffecthook detects the change and saves todos tolocalStorage. - The todos array is mapped to render
Itemcomponents. - User can complete or delete todos, which updates the state and triggers re-renders.
- The
-
Key Takeaways:
useStatemanages component-level state for interactive UI elements.useEffecthandles side effects like persisting data tolocalStorage.localStorageprovides persistent storage that survives page refreshes.- Array methods (
.map(),.filter()) are used for immutable state updates. - Event handlers (
onChange,onClick,onKeyDown) enable user interaction. - Immutability is crucial for React state updates - always create new arrays/objects.
- Unique IDs are essential for React's reconciliation algorithm and for identifying items in operations.
- Conditional rendering allows dynamic UI updates based on state.