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.

building simple to do app react

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 called addTask. This prop is a function that will be called when the form is submitted.
  • We use the useState hook to create a state variable called task and a function called setTask to update the state variable. The initial value of task 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 the addTask function with the current value of task, and resets the value of task 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 the onChange event updates the state variable task 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 the Form component we created earlier.
  • We define a functional component called App.
  • We use the useState hook to create a state variable called tasks and a function called setTasks to update the state variable. The initial value of tasks is an empty array.
  • We define a function called addTask that takes a task as an argument and adds it to the tasks 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 the map 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 and deleteTask. 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) and completed (a boolean value indicating whether the task is completed or not).
  • In the toggleTask function, we create a copy of the tasks array using the spread operator. We then toggle the completed 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 the tasks array using the spread operator. We then use the splice 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 the completed property of the task. We also use the style 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 the toggleTask 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 add e.stopPropagation() to prevent the click event from propagating to the list item and triggering the toggleTask 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 the tasks array using the spread operator. We then use the splice 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 add e.stopPropagation() to prevent the click event from propagating to the list item and triggering the toggleTask 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 called setFilter to update the state variable. The initial value of filter is 'all'.
  • We define a new variable called filteredTasks that is created by filtering the tasks array based on the value of the filter state variable. If the filter value is 'all', we return all tasks. If the filter value is 'completed', we return only completed tasks. If the filter 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 the tasks 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 the App.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, the styles.title class for the title, the styles.button class for the buttons, the styles.list class for the list, the styles.listItem class for the list items, the styles.checkbox class for the checkboxes, the styles.text class for the task text, and the styles['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.