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
useState
hook from React. This hook allows us to add state to our functional components. - We define a functional component called
Form
that takes a prop calledaddTask
. This prop is a function that will be called when the form is submitted. - We use the
useState
hook to create a state variable calledtask
and a function calledsetTask
to update the state variable. The initial value oftask
is an empty string. - We define a function called
handleSubmit
that is called when the form is submitted. This function prevents the default form submission behavior, calls theaddTask
function with the current value oftask
, and resets the value oftask
to 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 theonChange
event updates the state variabletask
with 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
useState
hook from React and theForm
component we created earlier. - We define a functional component called
App
. - We use the
useState
hook to create a state variable calledtasks
and a function calledsetTasks
to update the state variable. The initial value oftasks
is an empty array. - We define a function called
addTask
that takes a task as an argument and adds it to thetasks
array by using the spread operator. - We return a div element that contains an h1 element with the title of our app, the
Form
component, and an unordered list element. We use themap
function 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:
toggleTask
anddeleteTask
. These functions will be called when the user clicks the checkbox or the delete button. - In the
addTask
function, 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
toggleTask
function, we create a copy of thetasks
array using the spread operator. We then toggle thecompleted
property of the task at the given index by using the!
operator. Finally, we update the state with the new array of tasks. - In the
deleteTask
function, we create a copy of thetasks
array using the spread operator. We then use thesplice
method 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
checked
attribute to determine whether the checkbox should be checked or not based on thecompleted
property of the task. We also use thestyle
attribute 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
onClick
event handler to the list item itself. This event handler calls thetoggleTask
function with the index of the task. - We use the
style
attribute 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
onClick
event handler. We also adde.stopPropagation()
to prevent the click event from propagating to the list item and triggering thetoggleTask
function.
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
deleteTask
that takes the index of the task as an argument. Inside this function, we create a copy of thetasks
array using the spread operator. We then use thesplice
method 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
onClick
event handler. We also adde.stopPropagation()
to prevent the click event from propagating to the list item and triggering thetoggleTask
function.
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
filter
and a function calledsetFilter
to update the state variable. The initial value offilter
is'all'
. - We define a new variable called
filteredTasks
that is created by filtering thetasks
array based on the value of thefilter
state variable. If thefilter
value is'all'
, we return all tasks. If thefilter
value is'completed'
, we return only completed tasks. If thefilter
value 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
setFilter
function with the corresponding filter value when clicked. - We update the JSX code for the list items to use the
filteredTasks
array instead of thetasks
array.
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
styles
object from theApp.module.css
file. - We add the appropriate class names to the HTML elements using the
className
attribute. - We use the
styles.container
class for the main container, thestyles.title
class for the title, thestyles.button
class for the buttons, thestyles.list
class for the list, thestyles.listItem
class for the list items, thestyles.checkbox
class for the checkboxes, thestyles.text
class 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.