To-Do List Application in React JS
A To-Do List Application is a practical React JS project that allows users to create, view, complete, edit, and delete tasks. It helps learners understand important React concepts such as components, JSX, state management, event handling, forms, list rendering, conditional rendering, and data persistence.
React JS Training Course: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo
1. Learning Objectives
After completing this topic, learners will be able to:
- Understand the purpose of a To-Do List Application.
- Create functional components in React JS.
- Manage tasks using the
useState() Hook.
- Add new tasks through an input field and form.
- Display tasks dynamically using the JavaScript
map() method.
- Mark tasks as completed or incomplete.
- Edit and delete existing tasks.
- Filter tasks by their completion status.
- Use unique keys when rendering lists.
- Store and retrieve tasks using browser local storage.
- Build a responsive and user-friendly task management interface.
2. What Is a To-Do List Application?
A To-Do List Application is a task management tool that helps users organize daily activities. Users can add tasks, track their progress, mark tasks as completed, and remove tasks that are no longer required.
For example, a student might create the following tasks:
- Complete React JS notes.
- Practice JavaScript programs.
- Build a portfolio website.
- Revise interview questions.
Each task can be displayed in a list with controls for completing, editing, or deleting it.
Common Features
| Feature |
Description |
| Add Task |
Creates a new task from the user's input. |
| View Tasks |
Displays all saved tasks in a list. |
| Complete Task |
Marks a task as completed or incomplete. |
| Edit Task |
Allows the user to update an existing task. |
| Delete Task |
Removes a task from the list. |
| Filter Tasks |
Displays all, active, or completed tasks. |
| Task Counter |
Shows the number of pending tasks. |
| Local Storage |
Preserves tasks across browser refreshes on the same browser and origin. |
3. Technologies Used
| Technology |
Purpose |
| React JS |
Builds the user interface using reusable components. |
| JavaScript |
Handles task operations, filtering, and application logic. |
| JSX |
Describes the interface using HTML-like syntax in JavaScript. |
| HTML |
Provides semantic elements such as forms, buttons, and lists. |
| CSS |
Styles the application and makes it responsive. |
| useState() |
Manages tasks, input values, and editing state. |
| useEffect() |
Synchronizes task data with browser local storage. |
| localStorage |
Stores task data in the browser between page reloads. |
4. Prerequisites
Before building the application, learners should understand:
- HTML elements and forms.
- CSS selectors, spacing, flexbox, and responsive layouts.
- JavaScript arrays, objects, functions, and arrow functions.
- Array methods such as
map() and filter().
- JavaScript event handling.
- React functional components, JSX, and Hooks.
- Basic npm commands and project structure.
5. Understanding State in a To-Do List
State is information that a React component remembers between renders. When state changes through its setter function, React can re-render the component and display the updated interface.
A To-Do List usually requires state for the task array, the current input, and the task being edited.
import { useState } from 'react';
function TodoApp() {
const [tasks, setTasks] = useState([]);
const [input, setInput] = useState('');
return (
<div>
<h1>My To-Do List</h1>
<p>Total tasks: {tasks.length}</p>
<input
value={input}
onChange={event => setInput(event.target.value)}
placeholder="Enter a task"
/>
</div>
);
}
export default TodoApp;
Explanation
| Code |
Purpose |
useState([]) |
Initializes an empty array for tasks. |
tasks |
Stores the current task objects. |
setTasks |
Updates the task array. |
useState('') |
Initializes the task input with an empty string. |
input |
Stores the current text entered by the user. |
setInput() |
Updates the input state when the user types. |
6. Creating a React Project
If you do not already have a React project, create one using Vite. Make sure Node.js and npm are installed.
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev
Open the local URL displayed in the terminal to view the application.
Project Structure
todo-app/
├── public/
├── src/
│ ├── App.jsx
│ ├── App.css
│ ├── index.css
│ └── main.jsx
├── package.json
└── index.html
File responsibilities:
App.jsx contains the main To-Do List component.
App.css contains styles for the task interface.
index.css contains global styles.
main.jsx renders the React application.
package.json contains project dependencies and scripts.
7. Building the To-Do List Application Step by Step
Step 1: Create the Main Component
Replace the content of src/App.jsx with the following complete example. It supports adding, completing, editing, deleting, filtering, and counting tasks, along with local storage persistence.
import { useEffect, useState } from 'react';
import './App.css';
const STORAGE_KEY = 'react-todo-tasks';
function loadTasks() {
try {
const savedTasks = localStorage.getItem(STORAGE_KEY);
const parsedTasks = savedTasks ? JSON.parse(savedTasks) : [];
return Array.isArray(parsedTasks) ? parsedTasks : [];
} catch {
return [];
}
}
export default function App() {
const [tasks, setTasks] = useState(loadTasks);
const [input, setInput] = useState('');
const [editingId, setEditingId] = useState(null);
const [filter, setFilter] = useState('all');
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
} catch {
// Storage may be unavailable or full.
}
}, [tasks]);
function handleSubmit(event) {
event.preventDefault();
const text = input.trim();
if (!text) return;
if (editingId !== null) {
setTasks(currentTasks =>
currentTasks.map(task =>
task.id === editingId
? { ...task, text }
: task
)
);
setEditingId(null);
} else {
setTasks(currentTasks => [
...currentTasks,
{
id: crypto.randomUUID(),
text,
completed: false
}
]);
}
setInput('');
}
function handleEdit(task) {
setInput(task.text);
setEditingId(task.id);
}
function handleDelete(id) {
setTasks(currentTasks =>
currentTasks.filter(task => task.id !== id)
);
if (editingId === id) {
setEditingId(null);
setInput('');
}
}
function handleToggle(id) {
setTasks(currentTasks =>
currentTasks.map(task =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}
function cancelEdit() {
setEditingId(null);
setInput('');
}
function clearCompleted() {
setTasks(currentTasks =>
currentTasks.filter(task => !task.completed)
);
setEditingId(null);
setInput('');
}
const visibleTasks = tasks.filter(task => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
const completedCount = tasks.filter(
task => task.completed
).length;
const pendingCount = tasks.length - completedCount;
return (
<main className="todo-container">
<header className="todo-header">
<h1>My To-Do List</h1>
<p>Organize your tasks and stay productive.</p>
</header>
<form className="todo-form" onSubmit={handleSubmit}>
<label className="visually-hidden" htmlFor="task-input">
Task description
</label>
<input
id="task-input"
type="text"
value={input}
onChange={event => setInput(event.target.value)}
placeholder="What do you need to do?"
maxLength={200}
/>
<button type="submit">
{editingId !== null ? 'Save Task' : 'Add Task'}
</button>
{editingId !== null && (
<button type="button" className="secondary-button"
onClick={cancelEdit}>
Cancel
</button>
)}
</form>
<section className="todo-summary" aria-label="Task summary">
<span>Total: {tasks.length}</span>
<span>Pending: {pendingCount}</span>
<span>Completed: {completedCount}</span>
</section>
<nav className="todo-filters" aria-label="Filter tasks">
<button type="button"
aria-pressed={filter === 'all'}
onClick={() => setFilter('all')}>
All
</button>
<button type="button"
aria-pressed={filter === 'active'}
onClick={() => setFilter('active')}>
Active
</button>
<button type="button"
aria-pressed={filter === 'completed'}
onClick={() => setFilter('completed')}>
Completed
</button>
</nav>
{visibleTasks.length === 0 ? (
<p className="empty-message">
No tasks to display. Add a task to get started.
</p>
) : (
<ul className="task-list">
{visibleTasks.map(task => (
<li className="task-item" key={task.id}>
<label className="task-label">
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggle(task.id)}
/>
<span className={task.completed ? 'completed' : ''}>
{task.text}
</span>
</label>
<div className="task-actions">
<button type="button"
className="secondary-button"
onClick={() => handleEdit(task)}
disabled={task.completed}>
Edit
</button>
<button type="button"
className="delete-button"
onClick={() => handleDelete(task.id)}>
Delete
</button>
</div>
</li>
))}
</ul>
)}
{completedCount > 0 && (
<button type="button"
className="clear-button"
onClick={clearCompleted}>
Clear Completed Tasks
</button>
)}
</main>
);
}
Note: This example uses crypto.randomUUID() to generate task IDs. Use a modern browser that supports it or provide a suitable ID-generation alternative for older environments. Local storage is browser-specific and should not be treated as a secure database.
Step 2: Add CSS Styling
Replace the content of src/App.css with the following CSS:
.todo-container {
max-width: 760px;
margin: 48px auto;
padding: 30px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
}
.todo-header {
text-align: center;
margin-bottom: 26px;
}
.todo-header h1 {
margin-bottom: 8px;
color: #1f2937;
}
.todo-header p {
color: #6b7280;
}
.todo-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.todo-form input[type="text"] {
flex: 1;
min-width: 180px;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
}
button {
padding: 10px 14px;
border: none;
border-radius: 7px;
background: #2563eb;
color: #ffffff;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #1d4ed8;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
}
.todo-summary {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-around;
margin: 22px 0;
padding: 14px;
background: #f3f4f6;
border-radius: 8px;
color: #374151;
}
.todo-filters {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.todo-filters button[aria-pressed="true"] {
background: #1e40af;
}
.task-list {
padding: 0;
margin: 0;
list-style: none;
}
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 14px 0;
border-bottom: 1px solid #e5e7eb;
}
.task-label {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
overflow-wrap: anywhere;
}
.task-label input {
width: 17px;
height: 17px;
flex-shrink: 0;
}
.completed {
color: #6b7280;
text-decoration: line-through;
}
.task-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.secondary-button {
background: #4b5563;
}
.secondary-button:hover {
background: #374151;
}
.delete-button,
.clear-button {
background: #dc2626;
}
.delete-button:hover,
.clear-button:hover {
background: #b91c1c;
}
.clear-button {
margin-top: 20px;
}
.empty-message {
padding: 20px 0;
text-align: center;
color: #6b7280;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 600px) {
.todo-container {
margin: 18px 12px;
padding: 20px 16px;
}
.todo-form {
flex-direction: column;
}
.todo-form input[type="text"] {
width: 100%;
}
.task-item {
align-items: flex-start;
}
}
Step 3: Add Global CSS
In src/index.css, add the following global styles:
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: #f3f4f6;
}
Step 4: Run the Application
- Save all the files.
- Run
npm run dev in the project terminal.
- Open the local URL displayed in the terminal.
- Enter a task in the input field.
- Click Add Task to create it.
- Use the checkbox to mark a task as completed.
- Use Edit to modify a task and Delete to remove it.
- Use the All, Active, and Completed filters to view different tasks.
- Refresh the browser and check whether tasks remain saved.
8. How the Application Works
The To-Do List follows a state-driven workflow:
User enters a task
↓
Form submit event runs
↓
Input is validated
↓
Task array is updated
↓
React re-renders the component
↓
Updated task list is displayed
↓
useEffect saves tasks to localStorage
React uses state to describe what should appear on the screen. The application updates state in response to user actions rather than manually changing DOM elements. Learn more about managing state in React.
9. Adding a New Task
Adding a task requires an input field, a submit handler, and a state update.
function addTask(text) {
const newTask = {
id: crypto.randomUUID(),
text: text.trim(),
completed: false
};
setTasks(currentTasks => [
...currentTasks,
newTask
]);
}
Explanation:
id uniquely identifies the task.
text stores the task description.
completed indicates whether the task is complete.
- The spread syntax copies the existing array into a new array.
- The new task is appended to the task list.
Always validate the input using trim() so that a task containing only spaces is not added.
10. Displaying Tasks Using map()
The JavaScript map() method transforms each task object into a React element.
{tasks.map(task => (
<li key={task.id}>
{task.text}
</li>
))}
The key prop helps React identify individual items when a list changes. Use a stable, unique ID from the task data instead of the array index when tasks can be inserted, deleted, or reordered. Learn more about rendering lists in React.
11. Marking a Task as Completed
Each task has a Boolean property named completed. When the checkbox is selected, the application changes that property.
function toggleTask(id) {
setTasks(currentTasks =>
currentTasks.map(task =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}
The map() method creates a new array. The matching task is copied with its completion status reversed, while all other tasks remain unchanged.
When a task is completed, CSS can apply a line-through effect:
.completed {
text-decoration: line-through;
color: #6b7280;
}
12. Editing an Existing Task
Editing lets the user modify the text of a task without creating a new task.
function editTask(id, newText) {
const text = newText.trim();
if (!text) return;
setTasks(currentTasks =>
currentTasks.map(task =>
task.id === id
? { ...task, text }
: task
)
);
}
The example updates only the task with the matching ID. In the complete application, the Edit button loads the selected task's text into the input field, and the form changes to a Save Task button.
13. Deleting a Task
Deleting a task removes it from the array using the filter() method.
function deleteTask(id) {
setTasks(currentTasks =>
currentTasks.filter(task => task.id !== id)
);
}
The method creates a new array containing every task except the task with the specified ID. React then renders the updated list.
14. Filtering Tasks
Filtering allows users to display tasks according to their completion status.
| Filter |
Displayed Tasks |
| All |
Every task, completed or incomplete. |
| Active |
Tasks that are not completed. |
| Completed |
Tasks marked as completed. |
const visibleTasks = tasks.filter(task => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
The filtered list is calculated from the existing task state. There is no need to store a separate copy of the filtered tasks in state, because it can be derived from tasks and filter.
15. Counting Total, Pending, and Completed Tasks
Task counters provide a quick overview of progress.
const totalTasks = tasks.length;
const completedTasks = tasks.filter(
task => task.completed
).length;
const pendingTasks = totalTasks - completedTasks;
| Variable |
Meaning |
totalTasks |
Total number of tasks. |
completedTasks |
Number of completed tasks. |
pendingTasks |
Number of tasks still to complete. |
16. Saving Tasks with localStorage
React state normally exists in memory while the application is running. Browser local storage can preserve task data between page reloads on the same browser and origin.
Save Data
localStorage.setItem(
'react-todo-tasks',
JSON.stringify(tasks)
);
Read Data
const savedTasks = localStorage.getItem('react-todo-tasks');
const tasks = savedTasks ? JSON.parse(savedTasks) : [];
Synchronize State with localStorage
useEffect(() => {
localStorage.setItem(
'react-todo-tasks',
JSON.stringify(tasks)
);
}, [tasks]);
The complete application uses a lazy initializer to load tasks and a useEffect() Hook to save changes. It also catches storage errors so that unavailable or full storage does not immediately crash the component.
Important: Local storage is not a cloud database. Tasks are not automatically shared across devices or browser profiles, and users can clear this data. Do not store passwords, confidential information, or other sensitive data in local storage.
17. Important React Concepts Used in the Project
| Concept |
Usage |
| Functional Components |
Organize the application into reusable UI components. |
| JSX |
Defines the layout of the application. |
useState() |
Stores tasks, input text, filter selection, and editing information. |
useEffect() |
Synchronizes task data with local storage. |
| Event Handling |
Processes form submission, checkbox changes, and button clicks. |
map() |
Renders task objects as list elements. |
filter() |
Deletes tasks and filters tasks by status. |
| Conditional Rendering |
Displays empty-state messages and editing controls when needed. |
| Props |
Can pass task data and event handlers to child components. |
| Stable Keys |
Help React identify tasks during list updates. |
| Immutable Updates |
Create new arrays and objects instead of mutating state directly. |
18. Common Mistakes and Their Solutions
| Mistake |
Problem |
Solution |
| Using a normal variable for tasks |
Changes may not persist between renders or trigger updates. |
Use useState(). |
| Mutating the task array with push() |
Directly changes the existing state array. |
Use a new array with spread syntax or methods such as filter(). |
| Using array indexes as keys |
List updates can associate UI state with the wrong item. |
Use stable task IDs. |
| Accepting empty task descriptions |
Tasks containing only spaces can be added. |
Validate input with trim(). |
| Forgetting preventDefault() |
Submitting a form may reload the page. |
Call event.preventDefault() in the submit handler. |
| Not handling editing state |
Editing may accidentally create a duplicate task. |
Track the task ID being edited. |
| Saving tasks incorrectly |
Stored JSON may be malformed or unavailable. |
Handle parsing and storage errors. |
| Using a button without a type inside a form |
The button may submit the form unexpectedly. |
Use type="button" for non-submit actions. |
| Keeping duplicate derived state |
Counts or filtered tasks can become inconsistent. |
Calculate derived values from the main task state. |
19. Best Practices
- Use meaningful names such as
tasks, setTasks, handleSubmit, and handleDelete.
- Keep task updates immutable by creating new arrays and objects.
- Use unique IDs to identify tasks reliably.
- Validate user input before adding or editing tasks.
- Separate reusable components such as
TodoForm, TodoItem, and TodoList as the project grows.
- Use semantic HTML, descriptive labels, and accessible button names.
- Use
aria-pressed or another accessible indicator for active filters.
- Display a helpful empty-state message when no tasks match the selected filter.
- Keep derived information, such as task counts, calculated from the source state.
- Test editing, deleting, filtering, and persistence, not only adding tasks.
- Use local storage for small browser-based projects and a backend for shared or account-based task management.
20. Testing Checklist
- ☐ The application loads with no tasks when storage is empty.
- ☐ A valid task can be added.
- ☐ Empty or whitespace-only tasks are rejected.
- ☐ Multiple tasks can be added.
- ☐ A task can be marked completed and incomplete.
- ☐ A task can be edited and saved.
- ☐ Editing can be cancelled.
- ☐ A task can be deleted.
- ☐ The All filter displays every task.
- ☐ The Active filter displays only incomplete tasks.
- ☐ The Completed filter displays only completed tasks.
- ☐ Task counters update correctly.
- ☐ Completed tasks can be cleared.
- ☐ Tasks remain available after refreshing the page when local storage is available.
- ☐ The layout works on desktop and mobile screens.
- ☐ Buttons and inputs can be used with a keyboard.
21. Mini Project Enhancement Ideas
- Task priorities: Add High, Medium, and Low priority levels.
- Due dates: Let users assign a deadline to each task.
- Categories: Organize tasks into Work, Study, Personal, and Shopping.
- Search: Find tasks by matching their text.
- Sorting: Sort tasks by creation date, priority, or due date.
- Dark mode: Add a theme switcher.
- Undo delete: Allow users to restore a recently deleted task.
- Progress indicator: Display the percentage of completed tasks.
- Backend integration: Save tasks to a database through an API.
- User authentication: Add login functionality so each user can access their own tasks.
- Unit testing: Test task operations and important UI behavior.
- Drag-and-drop: Allow users to reorder tasks.
22. Interview Questions and Answers
Q1. What is a To-Do List Application?
Answer: It is a task management application that allows users to add, display, update, complete, and delete tasks.
Q2. Which React Hook is commonly used to manage tasks?
Answer: The useState() Hook stores the task array and other UI information.
Q3. Why do we use map() in React?
Answer: The map() method transforms an array of task objects into a collection of React elements.
Q4. Why does each task need a key?
Answer: A stable key helps React identify each list item when tasks are added, removed, or reordered.
Q5. What is the purpose of filter()?
Answer: The filter() method creates a new array containing only the items that satisfy a condition. It can be used to delete tasks from the state array or display active and completed tasks.
Q6. How do you mark a task as completed?
Answer: Update the selected task's completed property using map() and an immutable object update.
Q7. What is conditional rendering?
Answer: Conditional rendering displays different UI elements based on a condition, such as showing an empty-state message when there are no tasks.
Q8. What is localStorage?
Answer: It is browser storage that saves string key-value pairs and can preserve data across page reloads for the same origin.
Q9. Why is JSON.stringify() used?
Answer: It converts a JavaScript value, such as a task array, into a JSON string that can be stored in local storage.
Q10. Why is JSON.parse() used?
Answer: It converts a valid JSON string back into a JavaScript value.
Q11. What is the purpose of useEffect() in this project?
Answer: It synchronizes task state with browser local storage whenever the task array changes.
Q12. Why should state arrays not be mutated directly?
Answer: React state should be treated as read-only. Creating a new array or object makes state updates predictable and helps React detect changes.
Q13. What is the difference between state and props?
Answer: State is managed by a component, while props are values passed to a component by its parent.
Q14. Is localStorage suitable for a multi-user task management application?
Answer: Local storage is useful for a simple browser-only project, but a multi-user application generally needs a backend, database, and appropriate authentication and authorization.
Q15. How can you prevent duplicate task entries?
Answer: Validate input, trim whitespace, and optionally compare normalized task text with existing tasks before adding a new one.
23. Practice Exercises
- Create a basic To-Do List that supports adding and deleting tasks.
- Add a checkbox to mark tasks as completed.
- Add an Edit button to update existing task descriptions.
- Display total, pending, and completed task counts.
- Add All, Active, and Completed filters.
- Save tasks in local storage.
- Add task priorities and due dates.
- Create reusable
TodoForm, TodoList, and TodoItem components.
- Add search and sorting functionality.
- Write tests for the main task operations.
24. Quick Revision
| Concept |
Key Point |
| To-Do List |
Organizes and tracks tasks. |
useState() |
Stores tasks and user interface state. |
useEffect() |
Synchronizes task data with local storage. |
map() |
Renders the task list. |
filter() |
Filters or removes tasks. |
| Conditional Rendering |
Displays UI based on application state. |
| Unique Key |
Identifies list items reliably. |
| Immutable Updates |
Creates new arrays and objects instead of mutating state. |
| localStorage |
Stores small amounts of browser-specific data. |
| Form Handling |
Processes task input and submission. |
25. Official Documentation and Learning Resources
26. Conclusion
The To-Do List Application is an important beginner-to-intermediate React JS project. It teaches state management, forms, event handling, list rendering, immutable updates, conditional rendering, and local storage. By adding features such as editing, deletion, filtering, task counters, and persistence, learners develop practical skills that can be applied to larger projects such as project management tools, productivity dashboards, and task tracking systems.
To learn more, visit the JustAcademy React JS Training Course or register for a React JS course demo.