When we start learning a new programming language for web development, one of the best ways to truly grasp its concepts is by diving into a hands-on project. But why should we focus on projects that cover all the fundamental skills we need in the beginning? That's where a to-do list project comes into play. It's not just a task; creating a to-do list app will enhance your ReactJS skills giving you a solid foundation for future projects.
In this article, we will build a to-do app with ReactJS. In this app, we will be able to add items to the to-do list, edit items, and delete items as well. In addition, we will be able to mark a task as completed. We'll also include filters so users can choose to view all todos, completed todos, or not completed todos.
But wait, that's not all! For a better user experience, we will add a drag-and-drop feature that allows items to be repositioned easily. Most importantly, we'll save all our data in local storage so that even if you refresh the page or close the browser and open it again, your data will still be there.
It sounds interesting, doesn't it? I promise that by the end of this article, you will have a fully-featured To-do app. So without further ado, let’s build the project from scratch and understand it step by step.
Table of contents:
- Prerequisites
- Setting Up the Project
- Install required dependencies
- Project Structure
- How to build logic for this project
- Step-by-Step Implementation
Prerequisites
Before we start, ensure you have the following:
- Node.js and npm installed (If you haven't installed them yet, you can download them from here).
- Basic knowledge of React and React state management.
If you have everything ready, let's set up the project now.
Setting Up the Project
Create a new React Project using the following command in your terminal.
npx create-react-app todo-list-app
cd todo-list-app
When you use the command above to create a new React project, it sets up a folder structure for your project. You can clean up this structure for better understanding. In the public folder, keep only the index.html file and delete the rest. In the src folder, keep App.css, App.js, and index.js files, and delete the rest.
When you remove these files, it doesn't mean they're useless. Each file has its own specific purpose, but for this project, our goal is simplicity.
Now that we've deleted some files, we need to adjust the code in a few others. In the App.css file, delete all the existing code. In App.js and index.js, replace the code with the following:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
// src/App.js
import React from 'react'
function App() {
return (
<div>App</div>
)
}
export default App;
Now, let's install the dependencies we need for creating our To-Do app.
Install required dependencies:
We will use hello-pangea/dnd for drag-and-drop functionality and react-icons for icons.
npm install @hello-pangea/dnd react-icons
Make sure these dependencies are properly installed in your project. You can check this by opening the package.json file and looking inside the dependencies section. If the installation is successful, then you will see the package names and their current versions.
Project Structure
Now, let's create the folders and files we need for this project.
- Navigate to the src folder in your project directory.
- Create a new folder named components inside the src folder.
- Inside the components folder, create two files named TodoItem.jsx and TodoList.jsx.
After creating these files and folders, your project structure should look like this.
Now that we have our folder structure set up, our project setup complete, and our dependencies installed, it's time to create the logic for our project. Once we've built the logic for the To-Do list project, we can proceed to write the code for it.
How to build logic for this project
Before we start coding, let's go over the main ideas behind our Todo List App. Knowing this will help us add, edit, delete, mark as complete, reorder, and filter our to-do items easily.
State Management: We will use the useState hook to manage the state of the todos. The todos will be stored in an array, with each todo having properties like id, text, and completed.
Local Storage: To persist the todos, we will use localStorage. This ensures that the todos are saved even if the browser is refreshed.
Adding Todos: A new to-do item will be added to the array when the user enters text and clicks the 'Add' button.
Editing Todos: We will allow inline editing of the to-do text. Users can click on the edit icon to change the text of a to-do item. The updated text will be saved immediately in the state.
Deleting Todos: For removing todos, users can click a delete icon associated with each to-do item. This action will remove the todo from our todos array.
Toggling Completion: Each todo will feature a checkbox that toggles its completion status between done and not done. This interaction will update the completed property of the relevant todo object.
Drag-and-Drop Reordering: To allow for intuitive task organization, we'll implement drag-and-drop functionality using @hello-pangea/dnd library. This library facilitates reordering todos by dragging them to new positions within the list.
Filtering Todos: We will implement filters to display all, completed, or not completed todos based on the user's selection.
We hope you now understand the logic behind making your to-do list. Now it's easy to build a to-do list app with all the essential features by following these steps. Let's start coding each component now.
Step-by-Step Implementation
First, let's write the code for the TodoItem.jsx component.
In the TodoItem.jsx component, we'll add logic for editing, deleting, and marking items as complete. We'll also integrate drag-and-drop functionality using the @hello-pangea/dnd library.
// src\components\TodoItem.jsx
import React, { useState } from 'react';
import { Draggable } from '@hello-pangea/dnd';
import { FaEdit, FaTrashAlt } from 'react-icons/fa';
const TodoItem = ({ todo, index, handleEdit, handleDelete, handleToggle }) => {
const [isEditing, setIsEditing] = useState(false);
const [newText, setNewText] = useState(todo.text);
// Function to handle saving the edited text
const handleSave = () => {
handleEdit(todo.id, newText);
setIsEditing(false);
};
return (
<Draggable draggableId={todo.id.toString()} index={index}>
{(provided) => (
<div
className="todo-item"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{isEditing ? (
<input
type="text"
value={newText}
onChange={(e) => setNewText(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
/>
) : (
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
)}
<div className="buttons">
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<FaEdit className="edit-icon" onClick={() => setIsEditing(true)} />
<FaTrashAlt className="delete-icon" onClick={() => handleDelete(todo.id)} />
</div>
</div>
)}
</Draggable>
);
};
export default TodoItem;
In this TodoItem.jsx Component, allows users to edit, delete, and mark tasks as complete. We used the Draggable component from the @hello-pangea/dnd library to enable drag-and-drop functionality. The component displays the todo text and includes an input for editing, a checkbox for marking it as complete, an edit icon for toggling edit mode, and a trash icon for deleting the item. In this component, the state is maintained for editing and new text and saves edits, toggles completion, and deletes the entire item.
Now, let's write the code for the TodoList.jsx component.
In the TodoList.jsx component, we'll implement functionalities such as editing, deleting, and marking items as complete within our to-do list. Also, we'll integrate drag-and-drop capabilities using the @hello-pangea/dnd library.
// src\components\TodoList.jsx
import React from 'react';
import { DragDropContext, Droppable } from '@hello-pangea/dnd';
import TodoItem from './TodoItem';
// TodoList Component
const TodoList = ({ todos, setTodos, handleEdit, handleDelete, handleToggle }) => {
// Function to handle the end of drag event
const handleDragEnd = (result) => {
// If the destination is not valid, do nothing
if (!result.destination) return;
// Create a new array from todos
const items = Array.from(todos);
// Remove the dragged item from its original position
const [reorderedItem] = items.splice(result.source.index, 1);
// Insert the dragged item at its new position
items.splice(result.destination.index, 0, reorderedItem);
// Update the todos state with the new order
setTodos(items);
};
return (
<div className="todo-list-container">
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="todos">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{todos.map((todo, index) => (
<TodoItem
key={todo.id}
todo={todo}
index={index}
handleEdit={handleEdit}
handleDelete={handleDelete}
handleToggle={handleToggle}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
};
export default TodoList;
In this TodoList.jsx component, we are managing a list of to-do items and allowing users to reorder them using drag-and-drop. We used the DragDropContext and Droppable components from the @hello-pangea/dnd library to enable this functionality. The handleDragEnd function handles the logic for updating the order of to-do items after a drag-and-drop action. Inside the TodoList, we map through the todos array and render a TodoItem component for each todo, passing down the necessary props for editing, deleting, and toggling completion. The provided.placeholder element helps maintain the layout during drag-and-drop actions.
Now, let's write the code for the App.js component.
In the App.js component, we will handle the main state and logic for the to-do list application.
// src\App.js
import React, { useState, useEffect } from 'react';
import TodoList from './components/TodoList';
import './App.css';
const App = () => {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
const [text, setText] = useState('');
// Load todos from localStorage on initial load
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem('todos'));
if (storedTodos) {
setTodos(storedTodos);
}
}, []);
// Save todos to localStorage whenever todos change
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// Add a new todo item
const handleAdd = () => {
if (text.trim() !== '') {
setTodos([...todos, { id: Date.now(), text, completed: false }]);
setText('');
}
};
// Edit an existing todo item
const handleEdit = (id, newText) => {
setTodos(todos.map(todo => (todo.id === id ? { ...todo, text: newText } : todo)));
};
// Delete a todo item with confirmation
const handleDelete = (id) => {
const confirmDelete = window.confirm('Are you sure you want to delete this item?');
if (confirmDelete) {
setTodos(todos.filter(todo => todo.id !== id));
}
};
// Toggle the completed status of a todo item
const handleToggle = (id) => {
setTodos(todos.map(todo => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)));
};
// Filter todos based on the selected filter
const filteredTodos = todos.filter(todo =>
filter === 'completed' ? todo.completed : filter === 'not_completed' ? !todo.completed : true
);
return (
<div className="app">
<h1>My To-Do List</h1>
{/* Input container for adding a new todo */}
<div className="input-container">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button className='add-btn' onClick={handleAdd}>Add</button>
</div>
{/* Filter buttons */}
<div className="filter-buttons">
<button className='hover-button' onClick={() => setFilter('all')}>All</button>
<button className='hover-button' onClick={() => setFilter('completed')}>Completed</button>
<button className='hover-button' onClick={() => setFilter('not_completed')}>Not Completed</button>
</div>
{/* Todo list */}
<TodoList
todos={filteredTodos}
setTodos={setTodos}
handleEdit={handleEdit}
handleDelete={handleDelete}
handleToggle={handleToggle}
/>
</div>
);
};
export default App;
Here's the explanation of the code below:
State Management: We use useState hook to manage todos, filter, and text. todos holds the list of tasks, the filter determines which tasks to show, and the text is for the new task input.
LocalStorage Integration: useEffect loads todos from localStorage when the app starts and saves todos to localStorage whenever they change.
Adding Todos: The handleAdd function adds a new todo if the input text is not empty.
Editing Todos: The handleEdit function updates the text of an existing todo.
Deleting Todos: The handleDelete function deletes a todo after confirming with the user.
Toggling Completion: The handleToggle function marks a todo as completed or not.
Filtering Todos: The filteredTodos variable filters the todos based on the selected filter (all, completed, or not completed).
The App component shows the title "My To-Do List," an input field with an add button for new todos, filter buttons, and the TodoList component for displaying, editing, deleting, and toggling todos. This setup creates a functional to-do list app with persistent data and basic task management features.
Now, let's integrate the App component into the index.js file. However, we have already integrated the App component into the index.js file. Still, it's a good idea to double-check that everything is set up correctly.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
);
Congratulations! We've successfully added the functionality to our Todo application. But hold on—we haven't styled it yet. Let's add some styles to improve the UI to make it look great.
Add the following style code to your App.css file to enhance the appearance of your Todo application.
/* src\App.css */
body {
background-color: rgb(18, 18, 18);
}
.app {
max-width: 480px;
border: 2px solid white;
margin: 0 auto;
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
padding: 2rem;
background: #9395d3;
margin-top: 15px;
border-radius: 10px;
}
.app h1 {
text-align: center;
color: white;
font-weight: bold;
font-size: 32px;
margin: 0 0 20px 0;
}
.input-container {
display: flex;
gap: 1rem;
}
.input-container input {
flex: 1;
outline: none;
border: none;
background-color: #fbfbfb;
padding: 12px 20px;
border-radius: 5px;
}
.input-container button {
padding: 0.5rem 1rem;
border: 2px solid #ffffff;
color: white;
border-radius: 5px;
transition: 0.1s;
}
.input-container button:hover {
background-color: #8789c1;
}
.filter-buttons {
margin: 1rem 0;
display: flex;
justify-content: end;
}
.hover-button {
padding: 10px 0 10px 10px;
font-size: 16px;
margin: 0 10px;
color: white;
background-color: transparent;
border: none;
cursor: pointer;
transition: 0.3s;
}
.add-btn {
background-color: transparent;
cursor: pointer;
}
.hover-button:hover {
transform: translateY(-3px);
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f2f2ff;
padding: 1rem;
border-radius: 5px;
border: 2px solid #6666a8;
margin-bottom: 5px;
}
.todo-item .buttons {
display: flex;
gap: 0.5rem;
padding-left: 5px;
}
.todo-item .buttons input {
margin-right: 0.5rem;
}
.todo-item .completed {
text-decoration: line-through;
color: #a3a3a3;
}
.delete-icon {
color: crimson;
margin-left: 0.5rem;
cursor: pointer;
font-size: 18px;
}
.edit-icon {
color: #237bd8;
cursor: pointer;
margin-left: 0.5rem;
font-size: 18px;
}
.delete-icon:hover {
color: rgb(202, 18, 55);
}
.edit-icon:hover {
color: #1f6dc0;
}
.todo-list-container {
max-height: 400px;
overflow-y: auto;
padding: 0 10px 0 0;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #9395d3;
}
::-webkit-scrollbar-thumb {
background: #6b6dab;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #5e6096;
}
Now, your UI should look like this and work as expected.
Conclusion
After reading this article well, we hope you've successfully created a full-featured To-do List app using ReactJS. In this project, we learned how to add to-do items, edit them, delete them, move them around by dragging, and filter the existing todos.
For better understanding and practice, you can add features like Due Dates, Reminders, Priority Levels, Subtasks, Collaborative Lists, and more. For the user interface, you can add more styles to make your UI look even more attractive.
Thank you for reading! If you have any questions about this article or web development, you can ask them in the question box given below, you will get the answer soon.