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:
npx create-react-app react-todo-appcd 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:
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 thetodos
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 thetodos
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, andJSON.parse
converts it back when retrieved. addTodo
Function: This function adds a new todo object (containing the text and aisCompleted
flag) to thetodos
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 itsisCompleted
property totrue
. It also creates a new array to avoid mutating the state.removeTodo
Function: This function removes a todo from thetodos
array using thesplice
method. It also creates a new array.- Rendering: The component renders the
TodoForm
andTodoList
components, passing the necessary props (likeaddTodo
,todos
,completeTodo
, andremoveTodo
).
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!