Logo Ekbal's Blog

Search Blog

index

Introduction

Todo list applications are a classic project for developers learning a new framework or language. They provide a practical way to understand core concepts like state management, user input handling, and component rendering. In this comprehensive guide, we’ll walk you through building a complete and functional todo list application using React. We’ll cover everything from setting up the project to persisting data in local storage, creating a polished user interface, and adding features like marking tasks as complete and deleting them.

Setting Up the Project

First, let’s create a new React project using Create React App. Open your terminal and run:

Terminal window
npx create-react-app react-todo-app
cd react-todo-app

This command bootstraps a new React project with all the necessary dependencies and configurations. Once the installation is complete, you can start the development server:

Terminal window
npm start

This will open your application in your default browser, typically at http://localhost:3000.

Project Structure

Let’s create a simple project structure to keep our code organized:

react-todo-app/
├── public/
├── src/
│ ├── components/
│ │ ├── TodoForm.js
│ │ ├── TodoItem.js
│ │ └── TodoList.js
│ ├── App.js
│ ├── App.css
│ └── index.js
├── package.json
└── README.md

Here’s a brief explanation of each file:

  • App.js: The main component that orchestrates the application.
  • App.css: CSS file for the main application.
  • components/: Directory to store reusable React components.
    • TodoForm.js: Component for adding new todos.
    • TodoItem.js: Component for displaying a single todo item.
    • TodoList.js: Component to display the list of todos.

Creating the TodoForm Component

The TodoForm component is responsible for taking user input and adding new todo items to the list. Create a file named TodoForm.js inside the components directory with the following code:

import React, { useState } from 'react';
function TodoForm({ addTodo }) {
const [value, setValue] = useState('');
const handleSubmit = e => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
className="input"
value={value}
placeholder="Add Todo..."
onChange={e => setValue(e.target.value)}
/>
</form>
);
}
export default TodoForm;

This component uses the useState hook to manage the input value. The handleSubmit function prevents the default form submission behavior, checks if the input is not empty, calls the addTodo function (which will be passed down from the parent component), and resets the input field.

Creating the TodoItem Component

The TodoItem component displays a single todo item and handles marking it as complete or deleting it. Create a file named TodoItem.js inside the components directory with the following code:

import React from 'react';
function TodoItem({ todo, index, completeTodo, removeTodo }) {
return (
<div
className="todo"
style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
>
{todo.text}
<div>
<button onClick={() => completeTodo(index)}>Complete</button>
<button onClick={() => removeTodo(index)}>x</button>
</div>
</div>
);
}
export default TodoItem;

This component receives the todo object, its index in the list, and the completeTodo and removeTodo functions (passed from the parent) as props. It renders the todo text and two buttons: one to mark the todo as complete and another to remove it. The textDecoration style is conditionally applied based on the isCompleted property of the todo.

Creating the TodoList Component

The TodoList component renders the list of todo items using the TodoItem component. Create a file named TodoList.js inside the components directory. For simplicity, we’ll move the completeTodo and removeTodo functions to the App.js and pass them down

import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, completeTodo, removeTodo }) {
return (
<div className="todo-list">
{todos.map((todo, index) => (
<TodoItem
key={index}
index={index}
todo={todo}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
))}
</div>
);
}
export default TodoList;

This component receives the todos array and the completeTodo and removeTodo functions as props. It uses the map method to iterate over the todos array and render a TodoItem component for each todo. The key prop is important for React to efficiently update the list.

Integrating Components in App.js

Now, let’s integrate these components into our main App.js file.

import React, { useState, useEffect } from 'react';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import './App.css';
function App() {
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
return JSON.parse(savedTodos);
} else {
return [];
}
});
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = text => {
const newTodos = [...todos, { text, isCompleted: false }];
setTodos(newTodos);
};
const completeTodo = index => {
const newTodos = [...todos];
newTodos[index].isCompleted = true;
setTodos(newTodos);
};
const removeTodo = index => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
return (
<div className="app">
<div className="todo-list">
<h1>Todo List</h1>
<TodoForm addTodo={addTodo} />
<TodoList
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
/>
</div>
</div>
);
}
export default App;

Here’s what’s happening in this code:

  • State Management: We use the useState hook to manage the todos array. The initial state is retrieved from local storage using a function that’s only called on the first render. If there are no saved todos, it starts with an empty array.
  • Local Storage Persistence: The useEffect hook is used to persist the todos array in local storage whenever it changes. This ensures that the todos are saved even when the browser is closed or refreshed. JSON.stringify converts the array to a string for storage, and JSON.parse converts it back when retrieved.
  • addTodo Function: This function adds a new todo object (containing the text and a isCompleted flag) to the todos array. It creates a new array using the spread operator (...) to avoid mutating the original state.
  • completeTodo Function: This function marks a todo as complete by setting its isCompleted property to true. It also creates a new array to avoid mutating the state.
  • removeTodo Function: This function removes a todo from the todos array using the splice method. It also creates a new array.
  • Rendering: The component renders the TodoForm and TodoList components, passing the necessary props (like addTodo, todos, completeTodo, and removeTodo).

Styling the Application

Let’s add some basic styling to our application using the App.css file.

.app {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
}
.todo-list {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 400px;
}
.todo {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo:last-child {
border-bottom: none;
}
.input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
cursor: pointer;
border-radius: 4px;
margin-left: 5px;
}
button:hover {
opacity: 0.8;
}

This CSS provides basic styling for the app container, the todo list, individual todo items, and the input field. Feel free to customize the styling to your liking.

Conclusion

Congratulations! You’ve successfully built a complete todo list application using React. This tutorial covered the basics of creating components, managing state, handling user input, and persisting data in local storage. You can now expand this application by adding more features, such as editing todos, filtering todos based on their completion status, or implementing drag-and-drop functionality. Experiment with different styling techniques to make the application visually appealing. The possibilities are endless! Remember to always focus on writing clean, maintainable, and well-documented code. Happy coding!