Building a to-do list application is a rite of passage for any frontend developer learning a new framework. It's the perfect project because it covers the most essential concepts: managing state, handling user input, and rendering lists of data. In this comprehensive guide, we'll build a complete, modern to-do app from scratch using React and its powerful Hooks API.
By the end of this tutorial, you will have a fully functional application that can:
- Add new tasks.
- Display a list of all tasks.
- Mark tasks as completed.
- Delete tasks.
- Save your tasks in the browser so they persist after a refresh!
Prerequisites
Before we start, make sure you have Node.js installed on your machine. This will give you access to npm
(Node Package Manager) and npx
, which we'll use to create our React project.
Step 1: Setting Up Your React Project
Open your terminal and run the following command to create a new React application named `react-todo-app`:
npx create-react-app react-todo-app
Once it's done, navigate into the new directory and start the development server:
cd react-todo-app
npm start
Your browser should automatically open to http://localhost:3000
, displaying the default React welcome page. Now we're ready to code!
Step 2: Building the Main App Component
Let's clean up the default `src/App.js` file and set up the basic structure and state for our application. We'll use the useState
hook to manage our list of tasks.
Replace the contents of src/App.js
with this:
import React, { useState } from 'react';
import './App.css';
function App() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleAddTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, { id: Date.now(), text: inputValue, completed: false }]);
setInputValue('');
}
};
const handleToggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleDeleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
React To-Do App
{todos.map(todo => (
-
handleToggleTodo(todo.id)}>
{todo.text}
))}
);
}
export default App;
We've defined our state for the list (`todos`) and the input field (`inputValue`). We've also created handler functions to add, toggle, and delete tasks. The JSX renders the input form and maps over the `todos` array to display each item.
Step 3: Adding a Little Style
A good application needs to look good. Let's add some basic styling to make our app more user-friendly. Replace the contents of src/App.css
with the following:
.app {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1a202c;
color: #e2e8f0;
}
.todo-container {
background-color: #1f2937;
padding: 2rem;
border-radius: 1rem;
width: 100%;
max-width: 500px;
box-shadow: 0 10px 15px rgba(0,0,0,0.3);
}
h1 {
color: #00C9FF;
text-align: center;
margin-bottom: 1.5rem;
}
.input-container {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
input[type="text"] {
flex-grow: 1;
padding: 0.75rem;
border-radius: 5px;
border: 1px solid #4a5568;
background-color: #2d3748;
color: #e2e8f0;
font-size: 1rem;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
background-color: #00C9FF;
color: #1a202c;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #00a8d6;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2d3748;
padding: 1rem;
border-radius: 5px;
margin-bottom: 0.5rem;
}
li.completed span {
text-decoration: line-through;
color: #a0aec0;
}
li span {
cursor: pointer;
flex-grow: 1;
}
li button {
background-color: #e53e3e;
color: white;
padding: 0.4rem 0.8rem;
}
li button:hover {
background-color: #c53030;
}
Step 4: Persisting Data with Local Storage
Our app works, but if you refresh the page, all your tasks disappear! We can fix this by using the browser's Local Storage. The useEffect
hook is perfect for this. It lets us perform side effects, like saving to or loading from local storage.
First, import useEffect
at the top of src/App.js
:
import React, { useState, useEffect } from 'react';
Next, add these two useEffect
hooks inside your `App` component, right after the state declarations:
// Load todos from local storage when the app starts
useEffect(() => {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}, []); // The empty array means this effect runs only once on mount
// Save todos to local storage whenever the todos state changes
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]); // This effect runs whenever the `todos` array changes
Conclusion
Congratulations! You have successfully built a complete To-Do List application with React. You've learned how to manage component state with useState
, perform side effects for data persistence with useEffect
, handle user events, and render dynamic lists of data. This project provides a solid foundation for building even more complex and powerful frontend applications.