Build a To-Do List App with Reminders Using React, Local Storage, and Tailwind CSS
A to-do list app with reminder notifications is a great project for beginners learning CRUD operations, local storage, and working with the Notification API. In this guide, you’ll create a responsive, priority-based to-do list with features like task creation, editing, and deadline-based notifications.
Features
- Add, edit, and delete tasks.
- Set deadlines and receive notifications.
- Mark tasks as completed.
- Sort tasks by priority or due date.
Tech Stack
- React: A JavaScript library for building user interfaces.
- Local Storage: For data persistence without a backend.
- Notification API: To send reminders to users.
- Tailwind CSS: For rapid and responsive UI design.
Learning Goals
- Master CRUD operations in React.
- Persist data using local storage.
- Trigger browser notifications using the Notification API.
- Style with Tailwind CSS for a modern look.
Step-by-Step Implementation
1. Project Setup
Create a new React project:
npx create-react-app to-do-reminders cd to-do-reminders
Install Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
Configure Tailwind CSS:
Updatetailwind.config.js
with:module.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}", ], theme: { extend: {}, }, plugins: [], };
Add Tailwind to your styles:
Updatesrc/index.css
:@tailwind base; @tailwind components; @tailwind utilities;
Start the development server:
npm start
2. App Structure
Organize the app into reusable components:
/src
├── components
│ ├── TaskForm.js # Handles task creation
│ ├── TaskList.js # Displays all tasks
│ ├── TaskItem.js # Renders individual tasks
├── App.js # Main application logic
├── index.js # Entry point
3. Build the Components
3.1 TaskForm Component
The TaskForm component lets users input task details (title, deadline, priority).
Code:
// src/components/TaskForm.js
import { useState } from "react";
const TaskForm = ({ addTask }) => {
const [title, setTitle] = useState("");
const [deadline, setDeadline] = useState("");
const [priority, setPriority] = useState("Low");
const handleSubmit = (e) => {
e.preventDefault();
if (!title) return alert("Task title is required");
addTask({
id: Date.now(),
title,
deadline,
priority,
completed: false
});
setTitle("");
setDeadline("");
setPriority("Low");
};
return (
<form onSubmit={handleSubmit} className="p-4 bg-gray-100 rounded-md shadow-md">
<h2 className="text-xl font-bold mb-4">Add New Task</h2>
<input
type="text"
placeholder="Task Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-2 mb-4 border rounded-md"
/>
<input
type="datetime-local"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
className="w-full p-2 mb-4 border rounded-md"
/>
<select
value={priority}
onChange={(e) => setPriority(e.target.value)}
className="w-full p-2 mb-4 border rounded-md"
>
<option value="Low">Low Priority</option>
<option value="Medium">Medium Priority</option>
<option value="High">High Priority</option>
</select>
<button className="w-full bg-blue-600 text-white py-2 rounded-md">
Add Task
</button>
</form>
);
};
export default TaskForm;
Understanding the TaskForm
Component in React
In this section, we’ll break down the TaskForm
Component, which is designed to let users add new tasks with details like a title, deadline, and priority level. This is a great way to learn about React hooks, form handling, and event-driven programming.
Key Concepts in This Code
1. What is useState
?
React’s useState
hook allows us to manage and update the state of variables inside a functional component. Here, we use useState
to track three pieces of information:
title
: The name of the task.deadline
: The due date and time for the task.priority
: The task's priority level (Low, Medium, or High).
2. How the Component Works
TaskForm
Props: This component accepts a single prop,addTask
, which is a function passed from the parent component. TheaddTask
function handles what happens when a new task is added.Form Structure:
- The form consists of:
- An input field for the task title.
- A date and time picker for the deadline.
- A dropdown menu for setting the priority level.
- A submit button to add the task.
- The form consists of:
3. Code Walkthrough
Here’s how each part of the code works:
State Management:
const [title, setTitle] = useState(""); const [deadline, setDeadline] = useState(""); const [priority, setPriority] = useState("Low");
title
,deadline
, andpriority
start with default values.- Each
useState
hook comes with a setter function (setTitle
,setDeadline
,setPriority
) to update the values.
Form Submission:
const handleSubmit = (e) => { e.preventDefault(); // Prevents the page from reloading if (!title) return alert("Task title is required"); // Call the parent function to add the task addTask({ id: Date.now(), // Generate a unique ID for the task title, deadline, priority, completed: false // Default status is "not completed" }); // Clear form fields setTitle(""); setDeadline(""); setPriority("Low"); };
handleSubmit
is triggered when the user clicks "Add Task."- Form Validation: The code ensures a task title is provided. If not, it shows an alert.
- Task Data: The task object includes:
- A unique ID (
id
). - The user-provided
title
,deadline
, andpriority
. - A default
completed
value offalse
.
- A unique ID (
User Inputs:
Title Input:
<input type="text" placeholder="Task Title" value={title} onChange={(e) => setTitle(e.target.value)} />
- Tracks what the user types and updates
title
in real-time.
- Tracks what the user types and updates
Deadline Picker:
<input type="datetime-local" value={deadline} onChange={(e) => setDeadline(e.target.value)} />
- Lets users pick a deadline. Updates
deadline
state when changed.
- Lets users pick a deadline. Updates
Priority Dropdown:
<select value={priority} onChange={(e) => setPriority(e.target.value)} > <option value="Low">Low Priority</option> <option value="Medium">Medium Priority</option> <option value="High">High Priority</option> </select>
- Allows users to select a priority level.
Form Layout:
<form onSubmit={handleSubmit} className="p-4 bg-gray-100 rounded-md shadow-md"> <h2 className="text-xl font-bold mb-4">Add New Task</h2>
- The form is styled using Tailwind CSS, making it visually appealing and responsive.
Submit Button:
<button className="w-full bg-blue-600 text-white py-2 rounded-md"> Add Task </button>
- A button triggers the
handleSubmit
function to submit the form.
- A button triggers the
3.2 TaskList Components
This components displays a list of tasks and handle actions like marking tasks as completed or deleting them.
// src/components/TaskList.js
import TaskItem from "./TaskItem";
const TaskList = ({ tasks, toggleComplete, deleteTask }) => {
return (
<div className="mt-4">
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
toggleComplete={toggleComplete}
deleteTask={deleteTask}
/>
))}
</div>
);
};
export default TaskList;
Understanding the TaskList
Component in React
The TaskList
component is a reusable React component designed to display a list of tasks. It serves as the "middleman" between the app’s task data and individual task components. Let's explains the code in detail, highlighting its use of props, reusable components, and React's map()
method for rendering lists.
Key Concepts in This Code
1. What Does the TaskList
Component Do?
The TaskList
component:
- Receives a list of tasks as a prop.
- Iterates through the tasks using
map()
to render each task as a child component,TaskItem
. - Provides functionality for toggling task completion and deleting tasks, all through the props it passes to
TaskItem
.
Code Walkthrough
Importing Dependencies:
import TaskItem from "./TaskItem";
- The
TaskList
component importsTaskItem
, which is responsible for displaying the details of a single task.
- The
Component Declaration:
const TaskList = ({ tasks, toggleComplete, deleteTask }) => {
- The component is a functional component using arrow function syntax.
- Props:
tasks
: An array of task objects to display.toggleComplete
: A function to mark tasks as complete or incomplete.deleteTask
: A function to remove a task from the list.
Rendering the Task List:
return ( <div className="mt-4"> {tasks.map((task) => ( <TaskItem key={task.id} task={task} toggleComplete={toggleComplete} deleteTask={deleteTask} /> ))} </div> );
tasks.map()
:- React's
map()
function iterates over thetasks
array. - For each task object, it returns a
TaskItem
component.
- React's
key
Attribute:- React requires a unique
key
for list items to optimize rendering. Here,task.id
serves as the unique identifier.
- React requires a unique
- Props for TaskItem:
task
: Passes the task data to theTaskItem
component.toggleComplete
anddeleteTask
: Passes functions to handle task actions.
Styling:
<div className="mt-4">
- Uses Tailwind CSS for styling. The
mt-4
class adds margin to separate the task list visually.
- Uses Tailwind CSS for styling. The
Exporting the Component:
export default TaskList;
- Exports the component so it can be imported and used in other parts of the application.
How the Component Fits into Your Application
Parent-Child Relationship:
TaskList
is used in the main app component by importing it.- It receives
tasks
from the parent and passes individual task data to the child component,TaskItem
.
Dynamic Rendering:
- By using the
map()
function, the component dynamically renders the task list. If thetasks
array changes (e.g., a task is added or deleted), the component re-renders automatically.
- By using the
Benefits of This Approach
Separation of Concerns:
- The
TaskList
focuses only on iterating over tasks and rendering child components. Individual task details and actions are handled byTaskItem
.
- The
Reusability:
- The
TaskList
component can be reused wherever you need to display a task list, with different data or behaviors.
- The
Scalability:
- Since it works with dynamic data (
tasks
), the component can handle lists of any size.
- Since it works with dynamic data (
Performance Optimization:
- The
key
attribute ensures React efficiently updates the DOM, re-rendering only the modified components.
- The
3.3 TaskItem Components
This TaskItem component displays and manages an individual task
// src/components/TaskItem.js
const TaskItem = ({ task, toggleComplete, deleteTask }) => {
return (
<div
className={`p-4 mb-2 rounded-md shadow-md flex justify-between items-center ${
task.completed ? "bg-green-100" : "bg-gray-100"
}`}
>
<div>
<h3 className="text-lg font-bold">{task.title}</h3>
<p className="text-sm">Due: {new Date(task.deadline).toLocaleString()}</p>
<p className="text-sm">Priority: {task.priority}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => toggleComplete(task.id)}
className="px-4 py-2 bg-green-600 text-white rounded-md"
>
{task.completed ? "Undo" : "Complete"}
</button>
<button
onClick={() => deleteTask(task.id)}
className="px-4 py-2 bg-red-600 text-white rounded-md"
>
Delete
</button>
</div>
</div>
);
};
export default TaskItem;
Understanding the TaskItem
Component in React
The TaskItem
component is a reusable React component designed to display and manage an individual task. It combines task data with interactive functionality, allowing users to mark tasks as complete or delete them. Let's explains the code step by step, focusing on its structure, styling, and interactivity.
Key Concepts in This Code
1. What Does the TaskItem
Component Do?
The TaskItem
component:
- Displays details of a single task (title, deadline, and priority).
- Allows users to toggle the task's completion status.
- Provides a button to delete the task.
Code Walkthrough
Component Declaration:
const TaskItem = ({ task, toggleComplete, deleteTask }) => {
- The component is a functional component using arrow function syntax.
- Props:
task
: An object representing a single task with properties liketitle
,deadline
,priority
, andcompleted
.toggleComplete
: A function to mark the task as complete or undo completion.deleteTask
: A function to remove the task.
Main Container:
<div className={`p-4 mb-2 rounded-md shadow-md flex justify-between items-center ${ task.completed ? "bg-green-100" : "bg-gray-100" }`} >
- The main container is styled using Tailwind CSS:
- Dynamic Background Color:
- If
task.completed
istrue
, the background color is green (bg-green-100
). - Otherwise, it’s gray (
bg-gray-100
).
- If
- Styling Details:
- Padding (
p-4
), margin-bottom (mb-2
), and rounded corners (rounded-md
). - A shadow effect (
shadow-md
) for depth. - Flexbox properties (
flex
,justify-between
, anditems-center
) to align content horizontally and vertically.
- Padding (
- Dynamic Background Color:
- The main container is styled using Tailwind CSS:
Task Details:
<div> <h3 className="text-lg font-bold">{task.title}</h3> <p className="text-sm">Due: {new Date(task.deadline).toLocaleString()}</p> <p className="text-sm">Priority: {task.priority}</p> </div>
- Title: Displayed as a bold, larger font (
text-lg font-bold
). - Deadline:
- The deadline is converted to a human-readable format using JavaScript’s
toLocaleString()
method. - Why?: It ensures the date is displayed according to the user’s locale.
- The deadline is converted to a human-readable format using JavaScript’s
- Priority: Shows the task’s priority level.
- Title: Displayed as a bold, larger font (
Action Buttons:
<div className="flex items-center gap-2"> <button onClick={() => toggleComplete(task.id)} className="px-4 py-2 bg-green-600 text-white rounded-md" > {task.completed ? "Undo" : "Complete"} </button> <button onClick={() => deleteTask(task.id)} className="px-4 py-2 bg-red-600 text-white rounded-md" > Delete </button> </div>
- Container: A flexbox layout (
flex items-center gap-2
) aligns buttons horizontally with spacing between them. - "Complete/Undo" Button:
onClick
Handler: CallstoggleComplete(task.id)
when clicked.- Dynamic Text: If the task is completed, the button displays "Undo". Otherwise, it shows "Complete".
- Styling:
- Green background (
bg-green-600
), white text (text-white
), padding, and rounded corners.
- Green background (
- "Delete" Button:
onClick
Handler: CallsdeleteTask(task.id)
to remove the task.- Styling:
- Red background (
bg-red-600
), white text (text-white
), padding, and rounded corners.
- Red background (
- Container: A flexbox layout (
Exporting the Component:
export default TaskItem;
- The component is exported so it can be imported and used in the parent component, typically
TaskList
.
- The component is exported so it can be imported and used in the parent component, typically
How the Component Fits into Your Application
- Parent-Child Relationship:
TaskItem
is used inside theTaskList
component, which iterates over an array of tasks and renders aTaskItem
for each one.
- Interactive Functionality:
- The
toggleComplete
anddeleteTask
functions are passed down from the parent component, enablingTaskItem
to interact with the app’s state.
- The
Benefits of This Approach
Modular Design:
- The
TaskItem
component focuses solely on rendering and managing a single task, making it reusable and easier to maintain.
- The
Dynamic Styling:
- Conditional styling (e.g., green vs. gray background) visually differentiates completed tasks from incomplete ones.
Interactive UI:
- Users can mark tasks as complete or delete them, improving engagement and usability.
Readable Code:
- The code is clean and well-organized, with clear separation of responsibilities between the task details and action buttons.
Potential Enhancements
- Accessibility:
- Add
aria-label
attributes to buttons for better accessibility.
- Add
- Error Handling:
- Display a confirmation dialog before deleting a task to prevent accidental deletion.
- Animations:
- Use CSS animations or a library like Framer Motion to add transitions when tasks are marked as complete or deleted.
3.4 Main App Component
The App.js component integrates everything and uses localStorage for data persistence.
// src/App.js
import { useEffect, useState } from "react";
import TaskForm from "./components/TaskForm";
import TaskList from "./components/TaskList";
function App() {
const [tasks, setTasks] = useState(() => {
const savedTasks = localStorage.getItem("tasks");
return savedTasks ? JSON.parse(savedTasks) : [];
});
const addTask = (task) => {
setTasks((prevTasks) => [...prevTasks, task]);
};
const toggleComplete = (taskId) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === taskId ? { ...task, completed: !task.completed } : task
)
);
};
const deleteTask = (taskId) => {
setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId));
};
const sendNotification = (task) => {
if (Notification.permission === "granted") {
new Notification("Task Reminder", {
body: `Don't forget: ${task.title}`,
});
}
};
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
tasks.forEach((task) => {
if (!task.completed && task.deadline) {
const timeLeft = new Date(task.deadline).getTime() - Date.now();
if (timeLeft > 0) {
setTimeout(() => sendNotification(task), timeLeft);
}
}
});
}, [tasks]);
useEffect(() => {
if (Notification.permission !== "granted") {
Notification.requestPermission();
}
}, []);
return (
<div className="min-h-screen bg-gray-50 p-6">
<h1 className="text-3xl font-bold text-center mb-6">To-Do List App</h1>
<TaskForm addTask={addTask} />
<TaskList
tasks={tasks}
toggleComplete={toggleComplete}
deleteTask={deleteTask}
/>
</div>
);
}
export default App;
Understanding the App
Component in React
The App
component is the central hub of a To-Do List App. It combines various features like task management, local storage, notifications, and dynamic rendering. Let's explains the code in a beginner-friendly way, breaking down its structure, functionality, and concepts.
Key Concepts in This Code
1. What Does the App
Component Do?
- State Management: It tracks the app’s tasks using React's
useState
. - Task Actions: Allows users to add, complete, and delete tasks.
- Persistent Storage: Saves tasks in the browser's
localStorage
. - Notifications: Sends reminders for tasks with deadlines.
- Child Components: Delegates UI rendering to
TaskForm
(task creation) andTaskList
(task display).
Code Walkthrough
Importing Dependencies:
import { useEffect, useState } from "react"; import TaskForm from "./components/TaskForm"; import TaskList from "./components/TaskList";
useState
: Manages the state of tasks.useEffect
: Executes side effects like saving tasks or setting up notifications.TaskForm
: A form for adding new tasks.TaskList
: Displays a list of tasks and provides actions for each task.
State Initialization:
const [tasks, setTasks] = useState(() => { const savedTasks = localStorage.getItem("tasks"); return savedTasks ? JSON.parse(savedTasks) : []; });
- Initial State:
- Checks
localStorage
for saved tasks and loads them if available. - Otherwise, starts with an empty array.
- Checks
- Why Use a Callback?
- The callback ensures
localStorage
is only accessed during the initial render, improving performance.
- The callback ensures
- Initial State:
Task Management Functions:
Adding a Task:
const addTask = (task) => { setTasks((prevTasks) => [...prevTasks, task]); };
- Adds a new task to the existing task list using the spread operator (
...
).
- Adds a new task to the existing task list using the spread operator (
Toggling Completion:
const toggleComplete = (taskId) => { setTasks((prevTasks) => prevTasks.map((task) => task.id === taskId ? { ...task, completed: !task.completed } : task ) ); };
- Finds a task by its
id
and toggles itscompleted
status. - Uses the
map()
method to create a new array with the updated task.
- Finds a task by its
Deleting a Task:
const deleteTask = (taskId) => { setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId)); };
- Removes a task by filtering out the one with the matching
id
.
- Removes a task by filtering out the one with the matching
Task Notifications:
Sending Notifications:
const sendNotification = (task) => { if (Notification.permission === "granted") { new Notification("Task Reminder", { body: `Don't forget: ${task.title}`, }); } };
- Sends a browser notification if the user has granted permission.
Scheduling Notifications:
tasks.forEach((task) => { if (!task.completed && task.deadline) { const timeLeft = new Date(task.deadline).getTime() - Date.now(); if (timeLeft > 0) { setTimeout(() => sendNotification(task), timeLeft); } } });
- Calculates the time until the task deadline.
- Schedules a notification with
setTimeout()
if the task isn’t completed.
Effect Hooks:
Synchronizing State with
localStorage
:useEffect(() => { localStorage.setItem("tasks", JSON.stringify(tasks)); }, [tasks]);
- Saves the
tasks
array tolocalStorage
whenever it changes.
- Saves the
Requesting Notification Permissions:
useEffect(() => { if (Notification.permission !== "granted") { Notification.requestPermission(); } }, []);
- Requests permission for notifications when the app is first loaded.
Rendering the UI:
return ( <div className="min-h-screen bg-gray-50 p-6"> <h1 className="text-3xl font-bold text-center mb-6">To-Do List App</h1> <TaskForm addTask={addTask} /> <TaskList tasks={tasks} toggleComplete={toggleComplete} deleteTask={deleteTask} /> </div> );
- App Layout:
- A full-screen container (
min-h-screen
) with padding and a light background.
- A full-screen container (
- Header:
- Displays the app title.
- Child Components:
TaskForm
:- Handles task creation and passes the
addTask
function as a prop.
- Handles task creation and passes the
TaskList
:- Displays the tasks and passes action handlers (
toggleComplete
anddeleteTask
) as props.
- Displays the tasks and passes action handlers (
- App Layout:
Exporting the Component:
export default App;
- Makes the
App
component available for use in other parts of the application.
- Makes the
How the Component Fits into Your Application
- The
App
component serves as the root component, managing state and Application Logic. - It communicates with child components (
TaskForm
andTaskList
) to render the UI and handle interactions. - It ensures the app’s tasks persist across page reloads using
localStorage
.
Benefits of This Approach
- Stateful Logic:
- Centralized state (
tasks
) ensures data consistency across the app.
- Centralized state (
- Persistent Data:
localStorage
keeps tasks even if the user refreshes the page or closes the browser.
- Notifications:
- Adds a modern touch by reminding users about upcoming deadlines.
- Modularity:
- Separates task creation (
TaskForm
) and task display (TaskList
) into reusable components.
- Separates task creation (
Potential Enhancements
- Validation:
- Ensure task titles and deadlines are properly validated before adding tasks.
- Sorting and Filtering:
- Add functionality to sort tasks by priority or deadline.
- Allow users to filter completed or incomplete tasks.
- Dark Mode:
- Provide a dark theme for better user experience.
- Responsive Design:
- Optimize the layout for mobile devices.
Conclusion
Congratulations! You’ve built a to-do list app that lets users add tasks, set deadlines, and receive reminders. This project sharpens your skills in CRUD operations, local storage, and browser notifications. You can enhance the app further with sorting, filters, or integration with a backend.
Finished Code