Building a Dynamic To-Do List App with React: A Comprehensive Guide
Published: 2023-05-19 16:27:53

Are you looking for an efficient way to manage your tasks and improve your productivity? In this article, we will explore how to build a dynamic to-do list app using React, a popular JavaScript library for building user interfaces. We will walk through the code step by step, explaining its functionality and demonstrating how you can customize it to suit your needs.

## Prerequisites
Before we begin, make sure you have the following installed:
Node.js (version 14 or higher)
npm (Node Package Manager)

## STEP 1: Setting Up the Project
To start, create a new directory for your project and navigate to it in your terminal or command prompt. Run the following command to initialize a new React project:

```
npx create-react-app todo-list-app
cd todo-list-app

```

## STEP 2: Installing Dependencies
In this step, we need to install additional dependencies required for our Todo List app. Run the following command to install the required packages: 

```
npm install react-toastify react-modal react-toastify

```
## STEP 3: Setting up the Component
Open the src directory in your project and create a new file called TodoList.js. This file will contain our main component.

In the TodoList.js file, import the necessary dependencies and assets: 

```
import React, { useEffect, useState } from "react";
import { ToastContainer, toast } from 'react-toastify';
import notificationSound from './assets/Best-Reminder.mp3'
import notificationSound2 from './assets/OrchestralEmergencyAlarm.wav'
import 'react-toastify/dist/ReactToastify.css';
import Modal from 'react-modal';

```

## STEP 4: Defining the TodoList Component
Inside the TodoList.js file, define a functional component named TodoList using the arrow function syntax: 

```
const TodoList = () => {
  // Component logic will go here
}

```

## STEP 5: Initializing State Variables

Within the TodoList component, define state variables using the useState hook. These variables will store the tasks, input value, start times, finish times, modal state, edit index, edited value, completed tasks, and snooze times:

```
const [tasks, setTasks] = useState(JSON.parse(localStorage.getItem('tasks')) || []);
const [inputValue, setInputValue] = useState('');
const [startTimes, setStartTimes] = useState({});
const [finishTimes, setFinishTimes] = useState({});
const [modalIsOpen, setModalIsOpen] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [editIndex, setEditIndex] = useState(-1);
const [editedValue, setEditedValue] = useState('');
const [completedTasks, setCompletedTasks] = useState(JSON.parse(localStorage.getItem('completedTasks')) || []);
const [snoozeTimes, setSnoozeTimes] = useState({});
const audio = new Audio(notificationSound);
const audio2 = new Audio(notificationSound2);

```
In the above, we initialize the state variables with the values retrieved from localStorage or default values if no data is available.

## STEP 6: Configuring the Modal
We will use the Modal component from `react-modal` to create a modal dialog for editing tasks. Add the following code to configure the modal:

```
const customStyles = {
  content: {
    top: '50%',
    left: '50%',
    right: 'auto',
    bottom: 'auto',
    marginRight: '-50%',
    transform: 'translate(-50%, -50%)',
    width: '50%',
  },
};

Modal.setAppElement('#root');

```
In the above, we define custom styles for the modal and set the app element for accessibility purposes.

## STEP 7: Setting up Local Storage Effects
To persist our tasks and completed tasks in local storage, we will use the `useEffect` hook. Add the following code to store the tasks and completed tasks whenever they change:

```
useEffect(() => {
  localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);

useEffect(() => {
  localStorage.setItem('completedTasks', JSON.stringify(completedTasks));
}, [completedTasks]);

```
The first useEffect will store the tasks in local storage whenever it changes, and the second `useEffect` will do the same for the `completedTasks.`

## STEP 8: Handling Input Change
We need a handler function to update the input value as the user types. Add the following code to define the `handleInputChange` function:

```
const handleInputChange = e => {
  setInputValue(e.target.value);
};

```

## STEP 9: Handling Task Addition
To add a new task, we will define the `handleAddTask` function. This function will validate the input, add the task to the tasks array, and display a success or error toast message. Add the following code:

```
const handleAddTask = () => {
  if (inputValue.trim() !== '') {
    setTasks([...tasks, { id: Date.now(), text: inputValue, completed: false }]);
    setInputValue('');
    setStartTimes({ ...startTimes, [tasks.length]: null });
    setFinishTimes({ ...finishTimes, [tasks.length]: null });

    toast.success(`Task "${inputValue}" added successfully!`, {
      autoClose: 3000,
      position: toast.POSITION.TOP_CENTER,
    });
  } else {
    toast.error('Please enter a task', {
      autoClose: 3000,
      position: toast.POSITION.TOP_CENTER,
    });
  }
};

```

## STEP 10: Handling Task Deletion
To delete a task, we will define the `handleDeleteTask` function. This function will remove the task from the tasks array and associated start and finish times. It will also display a success toast message. Add the following code:

```
const handleDeleteTask = (index) => {
  setTasks(tasks.filter((task, i) => i !== index));
  const newStartTimes = { ...startTimes };
  const newFinishTimes = { ...finishTimes };
  delete newStartTimes[index];
  delete newFinishTimes[index];
  setStartTimes(newStartTimes);
  setFinishTimes(newFinishTimes);

  toast.success(`Your task has been deleted!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });
};

```

## STEP 11: Handling Task Editing

To edit a task, we will define the `handleEditTask` function. This function will set the edit index, pre-fill the modal with the task's current value, and open the modal. Add the following code:

```
const handleEditTask = (index) => {
  setEditIndex(index);
  setEditedValue(tasks[index].text);
  setModalIsOpen(true);
};

```

## STEP 12: Handling Task Completion
When a task is marked as completed, we will define the `handleCompleteTask` function. This function will update the task's completion status, display a success toast message, and move the completed task to the `completedTasks` array. Add the following code:

```
const handleCompleteTask = (index) => {
  const newTasks = [...tasks];
  newTasks[index].completed = true;
  setTasks(newTasks);

  toast.success(`Your task is completed!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });

  // Move the completed tasks to the history
  const completedTask = newTasks[index];
  setCompletedTasks([...completedTasks, completedTask]);
};

```

## STEP 13: Handling Modal Save and Close
For the modal functionality, we need to define the `handleModalSave` and `handleModalClose` functions. The `handleModalSave` function will update the edited task's value and close the modal, while the `handleModalClose` function will simply close the modal. Add the following code:

```
const handleModalSave = () => {
  const newTasks = [...tasks];
  newTasks[editIndex].text = editedValue;
  setTasks(newTasks);
  setModalIsOpen(false);

  toast.success(`Your task has been updated!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });
};

const handleModalClose = () => {
  setModalIsOpen(false);
};

```

## STEP 14: Handling Start and Finish Times
To set start and finish times for tasks, we will define the `handleSetStartTime` and `handleSetFinishTime` functions. These functions will update the start and finish times respectively and display success toast messages. Add the following code:

```
const handleSetStartTime = (index, startTime) => {
  setStartTimes({ ...startTimes, [index]: startTime });
  setSnoozeTimes({ ...snoozeTimes, [index]: startTime });

  toast.success(`Notification successfully set!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });
};

const handleSetFinishTime = (index, finishTime) => {
  setFinishTimes({ ...finishTimes, [index]: finishTime });

  toast.success(`Notification successfully set!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });
};

```

## STEP 15: Handling Completed Task Deletion
To delete a completed task, we will define the `handleDeleteCompletedTask` function. This function will remove the completed task from the `completedTasks` array. Add the following code:

```
const handleDeleteCompletedTask = (index) => {
  setCompletedTasks(completedTasks.filter((completedTask, i) => i !== index));
};

```

## STEP 16: Handling Snooze

To snooze a task, we will define the `handleSnoozeTask` function. This function will set a snooze time for the task and display an info toast message. Add the following code:

```
const handleSnoozeTask = (index, snoozeDuration) => {
  const now = new Date();
  const snoozeTime = new Date(now.getTime() + snoozeDuration * 60000); // Convert minutes to milliseconds

  setStartTimes({ ...startTimes, [index]: snoozeTime });
  setSnoozeTimes({ ...snoozeTimes, [index]: snoozeTime });

  toast.info(`Task "${tasks[index].text}" snoozed for ${snoozeDuration} minutes!`, {
    autoClose: 5000,
    position: toast.POSITION.TOP_CENTER,
  });
};

```

### Snooze Functionality
The `handleSnoozeTask` function is responsible for snoozing a task for a specified duration. When called, it receives the index of the task and the snoozeDuration in minutes. Here's how it works:

1. Get the current date and time using new Date().
2. Calculate the snooze time by adding the snooze duration (converted to milliseconds) to the current time.
3. Update the startTimes and snoozeTimes state by merging the existing values with the new snooze time using the spread operator (...).
4. Display a toast notification using a library like toast.info() to inform the user that the task has been snoozed. The notification includes the task's text and the snooze duration.

## STEP 17: Handling Task Reminders and Alerts
To handle task reminders and alerts, we will use the `useEffect` hook. Add the following code to set up the interval:

```
 useEffect(() => {
        const intervalID = setInterval(() => {
            const now = new Date();
            tasks.forEach((task, index) => {
                if (startTimes[index] && now >= startTimes[index]) {
                    audio.play();
    
                    toast.success(`You have two minutes left to start your task: ${task.text}`, {
                        autoClose: 5000,
                        position: toast.POSITION.TOP_CENTER,
                    });
    
                    setStartTimes({ ...startTimes, [index]: null });
                }
    
                if (finishTimes[index] && now >= finishTimes[index]) {
                    audio2.play();
                    toast.warning(`You have 2 minutes left to finish your task: ${task.text}`, {
                        autoClose: 5000,
                        position: toast.POSITION.TOP_CENTER,
                    });
    
                    setFinishTimes({ ...finishTimes, [index]: null });
                }
    
                if (snoozeTimes[index] && now >= snoozeTimes[index]) {
                    audio.play();
    
                    toast.success(`Time to start your snoozed task: ${task.text}`, {
                        autoClose: 5000,
                        position: toast.POSITION.TOP_CENTER,
                    });
    
                    setStartTimes({ ...startTimes, [index]: null });
                    setSnoozeTimes({ ...snoozeTimes, [index]: null });
                }
            });
        }, 1000);
    
        return () => clearInterval(intervalID);
    }, [tasks, startTimes, finishTimes, snoozeTimes]);
```

### Task Notifications
The `useEffect` hook is used to manage task notifications. It sets up a recurring interval (every second) to check the task statuses and display notifications when necessary. Here's how it works:

When the component mounts or any of the dependencies `(tasks, startTimes, finishTimes, or snoozeTimes)` change, the effect is triggered.

Within the effect, an interval is created using `setInterval()` to run the notification logic repeatedly.

For each task, the following checks are performed:
1. If the task's start time exists and the current time is greater than or equal to the start time, the notification is triggered using `toast.success().` The `audio` is played, and the start time for that task is cleared.

2. If the task's finish time exists and the current time is greater than or equal to the finish time, the notification is triggered using `toast.warning().` The `audio2` is played, and the finish time for that task is cleared.

3. If the task's snooze time exists and the current time is greater than or equal to the snooze time, the notification is triggered using `toast.success().` 

4. The audio is played, and both the start time and snooze time for that task are cleared.

5. Finally, the effect returns a cleanup function that clears the interval using `clearInterval()` when the component unmounts or when the dependencies change.

## STEP 18: Rendering the Todo List
In the return statement, we render the JSX for our Todo List app. It consists of a title, an input field for adding tasks, a button to show the completed tasks modal, and a list of tasks. Each task is displayed as an item with buttons to delete, edit, mark as done, set start and finish times, and snooze. The modal dialog is used for editing tasks. The completed tasks are shown in a separate modal dialog.

At the end of the component, we render the `ToastContainer` `component from `react-toastify` to display toast notifications. Finally, we will render the todo list in the `TodoList` component's JSX. Add the following code:

```
return (  
        <div className="todo-list">
            <h1>My To-Do List App</h1>
            <div className="inputAddTask">
                <div className="actionButtonSpace">
                    <input className="input" type="text" placeholder="Your Task" onChange={handleInputChange} value={inputValue} />
                    <button className="addTask" onClick={handleAddTask}>Add Task</button>
                    <button onClick={handleShowHistoryModal}>View Completed Tasks</button>
                </div>

                <ul className="action-buttons">
                    {tasks.map((task, index) => (
                        <li key={index} style={{ textDecoration: task.completed ? 'Line-Through' : 'none' }}>
                            {task.text}
                            <button className="deleteTask" onClick={() => handleDeleteTask(index)} style={{ background: 'red' }}>X</button>
                            <button className="editTask" onClick={() => handleEditTask(index)} disabled={task.completed} style={{ background: 'blue' }} >Edit</button>

                            {!task.completed && (
                                <button onClick={() => handleCompleteTask(index)} style={{ background: 'green' }}>Done</button>
                            )}
                            <input type="datetime-local" className="datetime-local" onChange={(e) => handleSetStartTime(index, new Date(e.target.value))} disabled={task.completed} />
                            <input type="datetime-local" className="datetime-local" onChange={(e) => handleSetFinishTime(index, new Date(e.target.value))} disabled={task.completed} />
                            <button onClick={() => handleSnoozeTask(index, 1)} disabled={task.completed}>Snooze</button>
                        </li>
                    ))}

                    <Modal isOpen={showHistoryModal} onRequestClose={handleCloseHistoryModal} contentLabel="Completed Tasks" >
                        <div className="history-modal">
                            <h2 className="historyHeader">Completed Tasks</h2>

                            {completedTasks.length > 0 ? (

                            <ul>
                                {completedTasks.map((task, index) => (
                                    <li key={index} className="completedTasks">
                                        <input className="checkBox" type="checkbox" checked="yes" style={{ background: 'green' }} />
                                        {task.text}
                                        <button style={{ backgroundColor: 'red'}} onClick={() => handleDeleteCompletedTask(index)}> X </button>
                                    </li>
                                ))}
                            </ul>

                            ) : (
                            <p className="zeroDoneTasks" style={{color: 'black'}}>No completed tasks.</p>
                            )}

                            <div className="Close-Modal">
                                <button  onClick={handleCloseHistoryModal}>Close</button>
                            </div>
                            
                        </div>
                </Modal>

                    <Modal isOpen={modalIsOpen} onRequestClose={handleModalClose} style={customStyles}>
                        <h2 className="editTask">Edit task</h2>
                        <div className="inputHeader">
                            <input className="inputMod " type="text" value={editedValue} onChange={(e) => setEditedValue(e.target.value)} />
                        </div>
                        <div className="modalAction">
                            <button className="modBut" onClick={handleModalSave}>Save</button>
                            <button className="modBut" onClick={handleModalClose}>Cancel</button>
                        </div>
                    </Modal>
                </ul>
            </div>


            <ToastContainer /> 
        </div>
    );
}    
```


## STEP 19: Exporting the TodoList Component

At the end of the TodoList.js file, export the `TodoList` component:

```
export default TodoList;

```

## Conclusion:
In this article, we have explored the code for building a to-do list app in React. We have seen how to manage tasks using state and local storage, handle user input, and implement features such as add task, view completed tasks, task deletion, editing, completion, setting time notifications and snooze feature. 

We have also utilized the react-toastify library for displaying toast notifications and the react-modal library for implementing modals. By understanding and customizing this code, you can create your own task management application with added features and functionality.

By leveraging the power of React and the available libraries, you can build robust and interactive applications to enhance task management and productivity in various domains.
Please note that this code assumes you have the required CSS styles and audio files in place. Make sure to update the file paths accordingly or provide your own styles and audio files.

Good luck with your Todo List app!

#react #javascript #beginners #reacthooks