Building a Simple To-Do App with React
This tutorial will guide you through building a simple to-do app using React. We will cover the basics of React, setting up a new project, building the user interface, managing state with React Hooks, adding functionality, and styling the app. By the end of this tutorial, you will have a fully functional to-do app that you can use as a starting point for your own projects.

Introduction
What is React?
React is a popular JavaScript library for building user interfaces. It allows developers to create reusable UI components and efficiently update the user interface as the data changes. React uses a virtual DOM (Document Object Model) to efficiently update only the parts of the UI that have changed, resulting in better performance and user experience.
Why use React for building a to-do app?
React's component-based architecture makes it easy to build and maintain complex user interfaces. It provides a clear separation of concerns, making it easier to reason about and test your code. React also has a large and active community, with many resources and libraries available to help you develop your app more quickly.
Setting Up the Project
Installing React
Before we start building our to-do app, we need to install React. You can install React and its dependencies using npm or yarn. Open your terminal and run the following command:
npm install react react-dom
Creating a new React project
Once React is installed, we can create a new React project using the Create React App tool. This tool sets up a new React project with all the necessary configuration and dependencies. Run the following command in your terminal:
npx create-react-app todo-app
This will create a new directory called todo-app with the basic project structure.
Setting up the project structure
Now that our project is set up, let's take a look at the project structure. Open the todo-app directory in your favorite code editor. Here's an overview of the important files and directories:
src: This directory contains the source code for our app.src/App.js: This is the main component of our app.src/index.js: This is the entry point of our app.public/index.html: This is the HTML template for our app.
Building the User Interface
Now that we have our project set up, let's start building the user interface for our to-do app.
Creating a form component
First, we'll create a form component that allows users to add new tasks to our to-do list. Create a new file called Form.js in the src directory. Add the following code to the Form.js file:
import React, { useState } from 'react';
const Form = ({ addTask }) => {
const [task, setTask] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
addTask(task);
setTask('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add Task</button>
</form>
);
};
export default Form;
Let's go through the code step by step:
- We import the
useStatehook from React. This hook allows us to add state to our functional components. - We define a functional component called
Formthat takes a prop calledaddTask. This prop is a function that will be called when the form is submitted. - We use the
useStatehook to create a state variable calledtaskand a function calledsetTaskto update the state variable. The initial value oftaskis an empty string. - We define a function called
handleSubmitthat is called when the form is submitted. This function prevents the default form submission behavior, calls theaddTaskfunction with the current value oftask, and resets the value oftaskto an empty string. - We return a form element with an input field and a submit button. The value of the input field is set to the current value of
task, and theonChangeevent updates the state variabletaskwith the new value entered by the user.
Handling form submission
Now that we have our form component, let's use it in our main app component to handle form submissions.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
const App = () => {
const [tasks, setTasks] = useState([]);
const addTask = (task) => {
setTasks([...tasks, task]);
};
return (
<div>
<h1>To-Do App</h1>
<Form addTask={addTask} />
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We import the
useStatehook from React and theFormcomponent we created earlier. - We define a functional component called
App. - We use the
useStatehook to create a state variable calledtasksand a function calledsetTasksto update the state variable. The initial value oftasksis an empty array. - We define a function called
addTaskthat takes a task as an argument and adds it to thetasksarray by using the spread operator. - We return a div element that contains an h1 element with the title of our app, the
Formcomponent, and an unordered list element. We use themapfunction to render each task as a list item.
Displaying the to-do list
Now that we have our form component and our main app component set up, we can see the to-do list in action.
Run the following command in your terminal to start the development server:
npm start
Open your browser and navigate to http://localhost:3000. You should see the title of your app and an input field with a submit button. Try entering some tasks in the input field and clicking the submit button. You should see the tasks appear as a list below the form.
Managing State with React Hooks
In the previous section, we used the useState hook to manage the state of our to-do list. In this section, we will explore the useState hook in more detail and learn how to update the state.
Using the useState hook
The useState hook allows us to add state to our functional components. It takes an initial value as an argument and returns an array with two elements: the current state value and a function to update the state value.
In our to-do app, we used the useState hook to create a state variable called tasks and a function called setTasks to update the state variable. Here's an example of how to use the useState hook:
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
In this example, we create a state variable called count with an initial value of 0. We also create two functions, increment and decrement, that update the value of count by calling the setCount function with the new value.
Updating the to-do list state
Now that we know how to use the useState hook, let's add the ability to mark tasks as completed and delete tasks from our to-do list.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
const App = () => {
const [tasks, setTasks] = useState([]);
const addTask = (task) => {
setTasks([...tasks, { text: task, completed: false }]);
};
const toggleTask = (index) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div>
<h1>To-Do App</h1>
<Form addTask={addTask} />
<ul>
{tasks.map((task, index) => (
<li key={index}>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(index)}
/>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
{task.text}
</span>
<button onClick={() => deleteTask(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We define two new functions:
toggleTaskanddeleteTask. These functions will be called when the user clicks the checkbox or the delete button. - In the
addTaskfunction, we now add each task as an object with two properties:text(the task itself) andcompleted(a boolean value indicating whether the task is completed or not). - In the
toggleTaskfunction, we create a copy of thetasksarray using the spread operator. We then toggle thecompletedproperty of the task at the given index by using the!operator. Finally, we update the state with the new array of tasks. - In the
deleteTaskfunction, we create a copy of thetasksarray using the spread operator. We then use thesplicemethod to remove the task at the given index. Finally, we update the state with the new array of tasks. - We update the JSX code to display the checkbox and the delete button for each task. We use the
checkedattribute to determine whether the checkbox should be checked or not based on thecompletedproperty of the task. We also use thestyleattribute to add a line-through text decoration to the task if it is completed.
Adding Functionality
Now that we have the ability to mark tasks as completed and delete tasks from our to-do list, let's add some additional functionality to our app.
Marking tasks as completed
Currently, we can toggle the completed property of a task by clicking the checkbox. However, it would be more user-friendly to allow users to click on the task text itself to mark it as completed.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
const App = () => {
const [tasks, setTasks] = useState([]);
const addTask = (task) => {
setTasks([...tasks, { text: task, completed: false }]);
};
const toggleTask = (index) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div>
<h1>To-Do App</h1>
<Form addTask={addTask} />
<ul>
{tasks.map((task, index) => (
<li
key={index}
onClick={() => toggleTask(index)}
style={{ textDecoration: task.completed ? 'line-through' : 'none' }}
>
{task.text}
<button onClick={(e) => {
e.stopPropagation();
deleteTask(index);
}}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We update the JSX code for each task to add an
onClickevent handler to the list item itself. This event handler calls thetoggleTaskfunction with the index of the task. - We use the
styleattribute to add a line-through text decoration to the task if it is completed. - We update the JSX code for the delete button to add an
onClickevent handler. We also adde.stopPropagation()to prevent the click event from propagating to the list item and triggering thetoggleTaskfunction.
Deleting tasks
In addition to marking tasks as completed, let's add the ability to delete tasks from our to-do list.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
const App = () => {
const [tasks, setTasks] = useState([]);
const addTask = (task) => {
setTasks([...tasks, { text: task, completed: false }]);
};
const toggleTask = (index) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div>
<h1>To-Do App</h1>
<Form addTask={addTask} />
<ul>
{tasks.map((task, index) => (
<li
key={index}
onClick={() => toggleTask(index)}
style={{ textDecoration: task.completed ? 'line-through' : 'none' }}
>
{task.text}
<button onClick={(e) => {
e.stopPropagation();
deleteTask(index);
}}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We define a new function called
deleteTaskthat takes the index of the task as an argument. Inside this function, we create a copy of thetasksarray using the spread operator. We then use thesplicemethod to remove the task at the given index. Finally, we update the state with the new array of tasks. - We update the JSX code for the delete button to add an
onClickevent handler. We also adde.stopPropagation()to prevent the click event from propagating to the list item and triggering thetoggleTaskfunction.
Filtering tasks
Now that we have the ability to mark tasks as completed and delete tasks from our to-do list, let's add the ability to filter the tasks based on their completion status.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
const App = () => {
const [tasks, setTasks] = useState([]);
const [filter, setFilter] = useState('all');
const addTask = (task) => {
setTasks([...tasks, { text: task, completed: false }]);
};
const toggleTask = (index) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
const filteredTasks = tasks.filter((task) => {
if (filter === 'all') {
return true;
} else if (filter === 'completed') {
return task.completed;
} else if (filter === 'active') {
return !task.completed;
}
});
return (
<div>
<h1>To-Do App</h1>
<Form addTask={addTask} />
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('completed')}>Completed</button>
<button onClick={() => setFilter('active')}>Active</button>
</div>
<ul>
{filteredTasks.map((task, index) => (
<li
key={index}
onClick={() => toggleTask(index)}
style={{ textDecoration: task.completed ? 'line-through' : 'none' }}
>
{task.text}
<button onClick={(e) => {
e.stopPropagation();
deleteTask(index);
}}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We define a new state variable called
filterand a function calledsetFilterto update the state variable. The initial value offilteris'all'. - We define a new variable called
filteredTasksthat is created by filtering thetasksarray based on the value of thefilterstate variable. If thefiltervalue is'all', we return all tasks. If thefiltervalue is'completed', we return only completed tasks. If thefiltervalue is'active', we return only active tasks (tasks that are not completed). - We update the JSX code to add three buttons for filtering the tasks. Each button calls the
setFilterfunction with the corresponding filter value when clicked. - We update the JSX code for the list items to use the
filteredTasksarray instead of thetasksarray.
Styling the App
Now that our to-do app is fully functional, let's add some styling to make it look more appealing.
Using CSS modules
CSS modules allow us to write CSS styles specific to a component without worrying about naming conflicts. We can use CSS modules with Create React App by naming our CSS files with the .module.css extension.
Create a new file called App.module.css in the src directory and add the following code:
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.title {
text-align: center;
font-size: 24px;
margin-bottom: 20px;
}
.form {
display: flex;
margin-bottom: 20px;
}
.input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.button {
padding: 10px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
.button:hover {
background-color: #0056b3;
}
.list {
list-style: none;
padding: 0;
}
.list-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #ccc;
}
.checkbox {
margin-right: 10px;
}
.text {
flex-grow: 1;
}
.text-completed {
text-decoration: line-through;
}
.delete-button {
padding: 5px;
border: none;
border-radius: 4px;
background-color: #dc3545;
color: #fff;
cursor: pointer;
}
.delete-button:hover {
background-color: #c82333;
}
This CSS file contains styles for our app, including the container, title, form, input field, button, list, list item, checkbox, task text, and delete button.
Adding custom styles
Now that we have our CSS file, let's import it into our App.js file and use the styles.
Open the src/App.js file and replace the existing code with the following:
import React, { useState } from 'react';
import Form from './Form';
import styles from './App.module.css';
const App = () => {
const [tasks, setTasks] = useState([]);
const [filter, setFilter] = useState('all');
const addTask = (task) => {
setTasks([...tasks, { text: task, completed: false }]);
};
const toggleTask = (index) => {
const newTasks = [...tasks];
newTasks[index].completed = !newTasks[index].completed;
setTasks(newTasks);
};
const deleteTask = (index) => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
const filteredTasks = tasks.filter((task) => {
if (filter === 'all') {
return true;
} else if (filter === 'completed') {
return task.completed;
} else if (filter === 'active') {
return !task.completed;
}
});
return (
<div className={styles.container}>
<h1 className={styles.title}>To-Do App</h1>
<Form addTask={addTask} />
<div>
<button className={styles.button} onClick={() => setFilter('all')}>All</button>
<button className={styles.button} onClick={() => setFilter('completed')}>Completed</button>
<button className={styles.button} onClick={() => setFilter('active')}>Active</button>
</div>
<ul className={styles.list}>
{filteredTasks.map((task, index) => (
<li
key={index}
onClick={() => toggleTask(index)}
className={styles.listItem}
>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(index)}
className={styles.checkbox}
/>
<span
className={`${styles.text} ${task.completed ? styles['text-completed'] : ''}`}
>
{task.text}
</span>
<button
onClick={(e) => {
e.stopPropagation();
deleteTask(index);
}}
className={styles['delete-button']}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Let's go through the code step by step:
- We import the
stylesobject from theApp.module.cssfile. - We add the appropriate class names to the HTML elements using the
classNameattribute. - We use the
styles.containerclass for the main container, thestyles.titleclass for the title, thestyles.buttonclass for the buttons, thestyles.listclass for the list, thestyles.listItemclass for the list items, thestyles.checkboxclass for the checkboxes, thestyles.textclass for the task text, and thestyles['delete-button']class for the delete button. - We use template literals to conditionally add the
styles['text-completed']class to the task text if it is completed.
Conclusion
In this tutorial, we have learned how to build a simple to-do app using React. We started by setting up a new React project and building the user interface. We then learned how to manage state with React Hooks and add functionality to mark tasks as completed and delete tasks. Finally, we added some styling to make our app look more appealing.
React provides a powerful and efficient way to build user interfaces. By breaking down our app into reusable components and managing state with React Hooks, we can build complex applications more easily and with less code. I hope this tutorial has given you a good introduction to React and inspired you to build your own React apps.