Are you tired of juggling multiple to-do lists, sticky notes, and scattered reminders? In today’s fast-paced world, staying organized is crucial. A well-designed to-do list can be your secret weapon for productivity, helping you manage tasks, track progress, and achieve your goals. In this tutorial, we’ll dive into building a simple, yet effective, interactive to-do list application using Next.js, a powerful React framework for building modern web applications. Whether you’re a beginner or an intermediate developer, this project will provide valuable hands-on experience with Next.js concepts and real-world application development.
Why Build a To-Do List with Next.js?
Next.js offers several advantages for this project:
- Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows us to render our to-do list on the server or generate it at build time. This improves SEO and initial load times, making our application faster and more accessible.
- Routing and Navigation: Next.js simplifies routing with its file-system based router, making it easy to navigate between different parts of our application, though our to-do list will be a single-page app.
- API Routes: Next.js provides a straightforward way to create API endpoints, which we’ll use to handle data persistence (saving and retrieving our to-do items).
- Developer Experience: Next.js offers a great developer experience with features like hot reloading, built-in CSS support, and easy deployment.
By using Next.js, we can create a performant, SEO-friendly, and user-friendly to-do list application.
Project Setup and Prerequisites
Before we begin, make sure you have the following installed on your system:
- Node.js and npm: You’ll need Node.js (version 14 or later) and npm (Node Package Manager) installed. You can download them from nodejs.org.
- Text Editor or IDE: Choose a text editor or integrated development environment (IDE) like VS Code, Sublime Text, or WebStorm.
- Basic JavaScript and React Knowledge: Familiarity with JavaScript and React fundamentals will be helpful, but this tutorial will explain the concepts as we go.
Let’s start by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app todo-app
This command will create a new Next.js project named “todo-app”. Navigate into the project directory:
cd todo-app
Now, let’s install a UI library (optional but recommended for styling). We’ll use Tailwind CSS for this project, as it simplifies styling:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
This will install Tailwind CSS and its peer dependencies, and generate the necessary configuration files. Next, configure Tailwind CSS by adding the paths to all of your template files in your `tailwind.config.js` file. Replace the content of the file with the following:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
// You can customize the theme here
},
},
plugins: [],
}
Add the Tailwind directives to your global stylesheet. Add the following directives to your `app/globals.css` file:
@tailwind base;
@tailwind components;
@tailwind utilities;
Finally, start the development server:
npm run dev
Open your browser and go to `http://localhost:3000`. You should see the default Next.js welcome page. Now, we’re ready to start building our to-do list application.
Building the To-Do List Components
We’ll break down the to-do list into several components to keep our code organized and maintainable. We will create the following components:
- `TodoInput` (Input Component): This component will handle the input field where users enter new tasks.
- `TodoItem` (Task Item Component): This component will display each individual to-do item, including the task text, completion status, and actions (e.g., delete).
- `TodoList` (List Component): This component will render the list of to-do items, managing their display and interaction.
- `Layout` (Layout Component): This component will define the overall layout of the application.
1. Creating the `TodoInput` Component
Create a new file named `TodoInput.js` inside a `components` folder in your `src` directory. If the `components` folder doesn’t exist, create it.
This component will contain an input field and a button to add new tasks. Here’s the code:
// src/components/TodoInput.js
import { useState } from 'react';
function TodoInput({ onAddTodo }) {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleAddTodo = () => {
if (inputValue.trim() !== '') {
onAddTodo(inputValue.trim());
setInputValue('');
}
};
return (
<div className="flex items-center mb-4">
<input
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="Add a task"
value={inputValue}
onChange={handleInputChange}
/>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleAddTodo}
>
Add
</button>
</div>
);
}
export default TodoInput;
In this component:
- We use the `useState` hook to manage the input field’s value (`inputValue`).
- `handleInputChange` updates the `inputValue` whenever the user types in the input field.
- `handleAddTodo` is called when the user clicks the “Add” button. It calls the `onAddTodo` function (passed as a prop) with the input value and clears the input field. The `onAddTodo` prop will be defined in the parent component, where this component is used.
2. Creating the `TodoItem` Component
Create a new file named `TodoItem.js` inside the `components` folder:
// src/components/TodoItem.js
function TodoItem({ todo, onDeleteTodo, onToggleComplete }) {
return (
<div className="flex items-center justify-between py-2 border-b border-gray-200">
<div className="flex items-center">
<input
type="checkbox"
className="mr-2"
checked={todo.completed}
onChange={() => onToggleComplete(todo.id)}
/>
<span className={todo.completed ? 'line-through text-gray-400' : 'text-gray-700'}>
{todo.text}
</span>
</div>
<button
className="text-red-500 hover:text-red-700"
onClick={() => onDeleteTodo(todo.id)}
>
Delete
</button>
</div>
);
}
export default TodoItem;
Here’s what this component does:
- It receives a `todo` object (containing `id`, `text`, and `completed` properties), `onDeleteTodo`, and `onToggleComplete` functions as props.
- It displays the task text, a checkbox to mark the task as complete, and a delete button.
- The checkbox’s `checked` attribute is bound to the `todo.completed` status.
- When the checkbox is clicked, `onToggleComplete` is called with the todo’s ID.
- When the delete button is clicked, `onDeleteTodo` is called with the todo’s ID.
3. Creating the `TodoList` Component
Create a new file named `TodoList.js` inside the `components` folder:
// src/components/TodoList.js
import TodoItem from './TodoItem';
function TodoList({ todos, onDeleteTodo, onToggleComplete }) {
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onDeleteTodo={onDeleteTodo}
onToggleComplete={onToggleComplete}
/>
))}
</div>
);
}
export default TodoList;
This component is responsible for rendering the list of to-do items:
- It receives an array of `todos`, `onDeleteTodo`, and `onToggleComplete` as props.
- It maps over the `todos` array and renders a `TodoItem` component for each to-do item.
- The `key` prop is important for React to efficiently update the list.
4. Creating the `Layout` Component
Create a new file named `Layout.js` inside the `components` folder:
// src/components/Layout.js
function Layout({ children }) {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">My To-Do List</h1>
{children}
</div>
);
}
export default Layout;
This component provides a basic layout structure for the application:
- It uses a container with some padding and a heading. You can modify this to fit your design.
- It renders the `children` prop, which will be the content of our to-do list.
Integrating Components in the `app/page.js` file
Now, let’s put everything together in your main page component (`app/page.js`):
// app/page.js
'use client';
import { useState, useEffect } from 'react';
import TodoInput from '../src/components/TodoInput';
import TodoList from '../src/components/TodoList';
import Layout from '../src/components/Layout';
function HomePage() {
const [todos, setTodos] = useState([]);
// Load todos from localStorage on component mount
useEffect(() => {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}, []);
// Save todos to localStorage whenever the todos state changes
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(), // Generate a unique ID
text: text,
completed: false,
};
setTodos([...todos, newTodo]);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const toggleComplete = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<Layout>
<TodoInput onAddTodo={addTodo} />
<TodoList
todos={todos}
onDeleteTodo={deleteTodo}
onToggleComplete={toggleComplete}
/>
</Layout>
);
}
export default HomePage;
Here’s what’s happening in `app/page.js`:
- Import Statements: We import the components we created (`TodoInput`, `TodoList`, `Layout`). Also, we import `useState` and `useEffect` from React.
- State Management: We use the `useState` hook to manage the `todos` array, which holds our to-do items.
- Loading Data from Local Storage: The `useEffect` hook, with an empty dependency array (`[]`), ensures that the code inside it runs only once when the component mounts. It retrieves the to-do list from `localStorage` and updates the `todos` state.
- Saving Data to Local Storage: Another `useEffect` hook is used to save the `todos` array to `localStorage` whenever the `todos` state changes. The dependency array `[todos]` ensures that this effect runs whenever the `todos` state is updated.
- `addTodo` Function: This function creates a new to-do item object with a unique ID (using `Date.now()`), the input text, and a `completed` status set to `false`. It then adds the new item to the `todos` array.
- `deleteTodo` Function: This function filters the `todos` array, removing the item with the specified ID.
- `toggleComplete` Function: This function toggles the `completed` status of a to-do item with the specified ID.
- JSX Structure: The component renders the `Layout` component and passes the `TodoInput` and `TodoList` components as children. It also passes the `addTodo`, `deleteTodo`, and `toggleComplete` functions as props to the respective components.
With these components in place, you should now have a functional to-do list application. You can add, delete, and mark tasks as complete.
Adding Functionality and Enhancements
Now that we have the basic functionality working, let’s add some enhancements to make our to-do list more user-friendly and feature-rich.
1. Styling with Tailwind CSS
We’ve already set up Tailwind CSS. Let’s apply some styles to improve the look and feel of our application. Here’s how you can enhance the components with Tailwind classes:
`TodoInput.js` (Updated):
// src/components/TodoInput.js
import { useState } from 'react';
function TodoInput({ onAddTodo }) {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleAddTodo = () => {
if (inputValue.trim() !== '') {
onAddTodo(inputValue.trim());
setInputValue('');
}
};
return (
<div className="flex items-center mb-4">
<input
type="text"
className="shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="Add a task"
value={inputValue}
onChange={handleInputChange}
/>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleAddTodo}
>
Add
</button>
</div>
);
}
export default TodoInput;
`TodoItem.js` (Updated):
// src/components/TodoItem.js
function TodoItem({ todo, onDeleteTodo, onToggleComplete }) {
return (
<div className="flex items-center justify-between py-2 border-b border-gray-200">
<div className="flex items-center">
<input
type="checkbox"
className="mr-2"
checked={todo.completed}
onChange={() => onToggleComplete(todo.id)}
/>
<span className={todo.completed ? 'line-through text-gray-400' : 'text-gray-700'}>
{todo.text}
</span>
</div>
<button
className="text-red-500 hover:text-red-700"
onClick={() => onDeleteTodo(todo.id)}
>
Delete
</button>
</div>
);
}
export default TodoItem;
`Layout.js` (Updated):
// src/components/Layout.js
function Layout({ children }) {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">My To-Do List</h1>
{children}
</div>
);
}
export default Layout;
These examples demonstrate how to use Tailwind CSS classes to style the components. You can customize the styles further to match your desired design.
2. Filtering To-Do Items
Let’s add a feature to filter to-do items based on their completion status (All, Active, Completed).
Modify the `app/page.js` file to include the filter functionality:
// app/page.js
'use client';
import { useState, useEffect } from 'react';
import TodoInput from '../src/components/TodoInput';
import TodoList from '../src/components/TodoList';
import Layout from '../src/components/Layout';
function HomePage() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// Load todos from localStorage on component mount
useEffect(() => {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}, []);
// Save todos to localStorage whenever the todos state changes
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text,
completed: false,
};
setTodos([...todos, newTodo]);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const toggleComplete = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') {
return !todo.completed;
}
if (filter === 'completed') {
return todo.completed;
}
return true;
});
return (
<Layout>
<TodoInput onAddTodo={addTodo} />
<div className="flex space-x-4 mb-4">
<button
className={`py-2 px-4 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={`py-2 px-4 rounded ${filter === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={`py-2 px-4 rounded ${filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
<TodoList
todos={filteredTodos}
onDeleteTodo={deleteTodo}
onToggleComplete={toggleComplete}
/>
</Layout>
);
}
export default HomePage;
Here’s what changed:
- We added a `filter` state variable to manage the current filter option.
- We added buttons to select the filter option (All, Active, Completed).
- We created a `filteredTodos` variable that filters the `todos` array based on the selected filter option.
- We passed the `filteredTodos` array to the `TodoList` component.
3. Clear Completed Tasks
Add a button to clear all completed tasks. Modify `app/page.js`:
// app/page.js
'use client';
import { useState, useEffect } from 'react';
import TodoInput from '../src/components/TodoInput';
import TodoList from '../src/components/TodoList';
import Layout from '../src/components/Layout';
function HomePage() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'
// Load todos from localStorage on component mount
useEffect(() => {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}, []);
// Save todos to localStorage whenever the todos state changes
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text,
completed: false,
};
setTodos([...todos, newTodo]);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const toggleComplete = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const clearCompleted = () => {
setTodos(todos.filter((todo) => !todo.completed));
};
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') {
return !todo.completed;
}
if (filter === 'completed') {
return todo.completed;
}
return true;
});
return (
<Layout>
<TodoInput onAddTodo={addTodo} />
<div className="flex space-x-4 mb-4">
<button
className={`py-2 px-4 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={`py-2 px-4 rounded ${filter === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={`py-2 px-4 rounded ${filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mb-4"
onClick={clearCompleted}
>
Clear Completed
</button>
<TodoList
todos={filteredTodos}
onDeleteTodo={deleteTodo}
onToggleComplete={toggleComplete}
/>
</Layout>
);
}
export default HomePage;
Here, we added the `clearCompleted` function, which filters out the completed tasks, and a button to trigger it.
Common Mistakes and Troubleshooting
Here are some common mistakes and how to fix them when building a Next.js to-do list:
- Incorrect Component Imports: Double-check that you’re importing components correctly. Make sure the file paths are accurate.
- State Not Updating: If the UI isn’t updating after a state change, ensure you’re using the correct state update functions (e.g., `setTodos`) and that the state variables are correctly passed as props to the child components.
- Local Storage Issues: Ensure that the browser’s local storage is not disabled or full. Also, verify that you’re correctly stringifying and parsing JSON when storing and retrieving data from `localStorage`.
- Uniqueness of Keys: When rendering lists in React, always provide a unique `key` prop to each element. This helps React efficiently update the DOM. Use a unique identifier (like the `id` in our example).
- CSS Styling Problems: If your CSS styles aren’t being applied, check for typos in class names, ensure the correct CSS files are imported, and verify that the CSS is being correctly applied to the components. In the case of Tailwind, ensure you have set up the configuration correctly.
Summary and Key Takeaways
In this tutorial, we’ve successfully built a simple, interactive to-do list application using Next.js. We covered the following key concepts:
- Project Setup: Setting up a Next.js project and installing necessary dependencies (like Tailwind CSS).
- Component Creation: Creating reusable components (`TodoInput`, `TodoItem`, `TodoList`, and `Layout`) to structure the application.
- State Management: Using the `useState` and `useEffect` hooks to manage the to-do items and persist data in `localStorage`.
- Event Handling: Handling user interactions, such as adding, deleting, and marking tasks as complete.
- Styling with Tailwind CSS: Applying styles to the components to improve the user interface.
- Adding Features: Implementing features like filtering tasks and clearing completed tasks.
This project provides a solid foundation for building more complex web applications with Next.js. You can expand on this project by adding features like:
- Data Persistence: Storing to-do items in a database instead of `localStorage`.
- Authentication: Adding user authentication to personalize the to-do list.
- Drag and Drop: Implementing drag-and-drop functionality to reorder tasks.
- Due Dates and Reminders: Adding due dates and reminder notifications.
- Integration with APIs: Integrating with external APIs to fetch or store data.
FAQ
Here are some frequently asked questions about building a to-do list with Next.js:
- Can I use a different UI library instead of Tailwind CSS?
Yes, you can use any UI library or framework you prefer, such as Bootstrap, Material UI, or styled-components. The core concepts of the to-do list application remain the same, regardless of the styling library you choose.
- How do I deploy my Next.js to-do list?
You can deploy your Next.js application to various platforms, including Vercel (which is recommended for Next.js), Netlify, AWS, or other hosting providers. The deployment process typically involves running `npm run build` to create a production-ready build and then deploying the contents of the `.next` folder.
- How can I make the to-do list accessible?
To make your to-do list accessible, follow these best practices:
- Use semantic HTML elements (e.g., <nav>, <article>, <aside>).
- Provide alternative text for images.
- Ensure sufficient color contrast.
- Use ARIA attributes to enhance accessibility.
- Test your application with a screen reader.
- How can I improve the performance of my to-do list?
To improve performance, consider these tips:
- Optimize images (use appropriate formats and sizes).
- Minimize the use of third-party libraries.
- Use code splitting to load only the necessary code for each page.
- Implement lazy loading for images and components.
- Use server-side rendering or static site generation for improved initial load times.
- Where can I learn more about Next.js?
You can find comprehensive documentation and tutorials on the official Next.js website: nextjs.org. Also, explore online courses, blog posts, and community forums.
Building this to-do list application is a stepping stone to becoming proficient in Next.js and React. The principles of component-based architecture, state management, and user interaction are fundamental to web development. With each project, you gain more confidence and expertise. Keep experimenting, exploring new features, and refining your skills. The journey of a software engineer is one of continuous learning, and with each line of code, you’re building not just applications, but also your own expertise and abilities.
