Tired of scattered sticky notes and endless mental to-do lists? In this tutorial, we’ll build a simple, yet effective, to-do list application using Next.js. This project is perfect for beginners and intermediate developers looking to solidify their understanding of Next.js fundamentals, including components, state management, and basic styling. We’ll create a functional and user-friendly application that allows you to add, mark as complete, and delete tasks. By the end of this guide, you’ll not only have a practical tool but also a solid foundation for building more complex web applications.
Why Build a To-Do List?
To-do lists are a fundamental part of staying organized and productive. They help us track tasks, prioritize our work, and reduce the mental burden of remembering everything. Building a to-do list application offers several benefits for developers:
- Practical Application: You create something useful that you can use daily.
- Learning Opportunity: You gain experience with core web development concepts.
- Foundation for Larger Projects: It provides a stepping stone to more complex applications.
This tutorial focuses on the essentials, making it an excellent starting point for your Next.js journey.
Prerequisites
Before we begin, ensure you have the following:
- Node.js and npm (or yarn): You’ll need these to manage project dependencies. Download and install them from the official Node.js website.
- A Code Editor: VS Code, Sublime Text, or any editor of your choice will work.
- Basic HTML, CSS, and JavaScript knowledge: Familiarity with these is helpful, but not strictly required.
Setting Up the Project
Let’s get started by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app todo-app
cd todo-app
This command creates a new Next.js project named “todo-app”. Navigate into the project directory using `cd todo-app`.
Project Structure Overview
Your project directory should look something like this:
todo-app/
├── node_modules/
├── pages/
│ └── _app.js
│ └── index.js
├── public/
├── .gitignore
├── next.config.js
├── package-lock.json
├── package.json
└── README.md
pages/: This directory holds your application’s pages. Each file in this directory represents a route (e.g., `pages/index.js` corresponds to the `/` route).public/: This directory contains static assets like images and fonts.package.json: This file lists your project’s dependencies and scripts.
Building the To-Do List Components
We’ll create several components to structure our application:
TodoInput: Handles user input for new tasks.TodoList: Displays the list of to-do items.TodoItem: Represents a single to-do item.
1. Creating the TodoInput Component
Create a new file named `components/TodoInput.js` inside your project directory. This component will contain an input field and a button to add new tasks.
// components/TodoInput.js
import React, { useState } from 'react';
const TodoInput = ({ onAddTodo }) => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
if (inputValue.trim() !== '') {
onAddTodo(inputValue.trim());
setInputValue('');
}
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Add a task..."
style={{ marginRight: '10px', padding: '5px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<button type="submit" style={{ padding: '5px 10px', borderRadius: '4px', border: 'none', backgroundColor: '#4CAF50', color: 'white', cursor: 'pointer' }}>
Add
</button>
</form>
);
};
export default TodoInput;
This component uses the `useState` hook to manage the input field’s value. The `handleChange` function updates the state as the user types, and the `handleSubmit` function adds the task to the list when the form is submitted. The `onAddTodo` prop is a function passed from the parent component (we’ll create this later) to handle adding new tasks.
2. Creating the TodoItem Component
Create a new file named `components/TodoItem.js`.
// components/TodoItem.js
import React from 'react';
const TodoItem = ({ todo, onToggleComplete, onDeleteTodo }) => {
return (
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '10px', padding: '10px', border: '1px solid #eee', borderRadius: '4px', justifyContent: 'space-between' }}>
<label style={{ flex: 1 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggleComplete(todo.id)}
style={{ marginRight: '10px' }}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</label>
<button onClick={() => onDeleteTodo(todo.id)} style={{ padding: '5px 10px', borderRadius: '4px', border: 'none', backgroundColor: '#f44336', color: 'white', cursor: 'pointer' }}>
Delete
</button>
</div>
);
};
export default TodoItem;
This component displays a single to-do item. It includes a checkbox to mark the task as complete (using the `onToggleComplete` function) and a delete button (using the `onDeleteTodo` function). The `todo` prop contains the task’s text and completion status.
3. Creating the TodoList Component
Create a new file named `components/TodoList.js`.
// components/TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
const TodoList = ({ todos, onToggleComplete, onDeleteTodo }) => {
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggleComplete={onToggleComplete}
onDeleteTodo={onDeleteTodo}
/>
))}
</div>
);
};
export default TodoList;
This component renders the list of to-do items. It iterates over the `todos` array (passed as a prop) and renders a `TodoItem` component for each item. It also passes the `onToggleComplete` and `onDeleteTodo` functions to each `TodoItem`.
Implementing the Main Page (index.js)
Now, let’s modify the `pages/index.js` file to integrate these components and manage the to-do list’s state.
// pages/index.js
import React, { useState, useEffect } from 'react';
import TodoInput from '../components/TodoInput';
import TodoList from '../components/TodoList';
const Home = () => {
const [todos, setTodos] = useState([]);
// Load todos from local storage on component mount
useEffect(() => {
if (typeof window !== 'undefined') {
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
setTodos(JSON.parse(storedTodos));
}
}
}, []);
// Save todos to local storage whenever the todos state changes
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('todos', JSON.stringify(todos));
}
}, [todos]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(), // Simple unique ID
text: text,
completed: false,
};
setTodos([...todos, newTodo]);
};
const toggleComplete = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2 style={{ textAlign: 'center' }}>My To-Do List</h2>
<TodoInput onAddTodo={addTodo} />
<TodoList
todos={todos}
onToggleComplete={toggleComplete}
onDeleteTodo={deleteTodo}
/>
</div>
);
};
export default Home;
Here’s what’s happening in `index.js`:
- Import Components: We import `TodoInput` and `TodoList`.
- State Management: We use the `useState` hook to manage the `todos` state, which is an array of to-do objects.
- `useEffect` for Local Storage: Two `useEffect` hooks handle saving and loading the to-do list to the browser’s local storage, ensuring that the list persists across page reloads. The first `useEffect` loads todos from local storage when the component mounts. The second `useEffect` saves todos to local storage whenever the `todos` state changes. The `typeof window !== ‘undefined’` check is crucial because Next.js pre-renders pages on the server, where the `window` object is not available.
- `addTodo` Function: This function adds a new to-do item to the `todos` array.
- `toggleComplete` Function: This function toggles the `completed` status of a to-do item.
- `deleteTodo` Function: This function removes a to-do item from the `todos` array.
- Rendering Components: We render the `TodoInput` and `TodoList` components, passing the necessary props to them.
Running the Application
To run your application, open your terminal, navigate to your project directory (if you’re not already there), and run the following command:
npm run dev
# or
yarn dev
This will start the development server. Open your web browser and go to `http://localhost:3000` to view your to-do list application.
Styling the Application
The current styling is very basic. Let’s add some basic CSS to make it look better. You can add styles directly to the components using inline styles or, for a more scalable approach, you can create a `styles` folder and import CSS modules. For simplicity, we’ll use inline styles in this tutorial. You can modify the inline styles in each component to customize the appearance.
Common Mistakes and Troubleshooting
Here are some common mistakes and how to fix them:
- Local Storage Errors: If you encounter errors related to local storage, ensure you’re only accessing `localStorage` within the browser environment. The `typeof window !== ‘undefined’` check is essential in Next.js.
- Incorrect State Updates: When updating the state, always create a new array or object instead of directly modifying the existing one. For example, use the spread operator (`…`) to create a new array when adding a new todo item.
- Missing Dependencies: Make sure you have installed all the necessary dependencies.
- Typographical Errors: Double-check your code for any typos, especially in component names and prop names.
Enhancements and Next Steps
Here are some ideas to enhance your to-do list application:
- Add Date/Time: Allow users to set deadlines for their tasks.
- Prioritization: Add priority levels (high, medium, low) to tasks.
- Filtering: Implement filters to show completed, incomplete, or all tasks.
- User Authentication: Integrate user authentication to allow multiple users to manage their to-do lists.
- Backend Integration: Connect your application to a database (e.g., Firebase, MongoDB) to store the data persistently.
- Styling with CSS-in-JS or CSS Modules: Improve the look and feel using a CSS framework like Tailwind CSS or Styled Components.
Key Takeaways
This tutorial has provided a practical guide to building a simple to-do list application using Next.js. You’ve learned how to:
- Create and structure Next.js components.
- Manage state with the `useState` hook.
- Handle user input and events.
- Persist data using local storage.
- Apply basic styling.
This project is a great starting point for understanding the fundamentals of Next.js and building interactive web applications. As you experiment with these concepts, you’ll gain the confidence and knowledge needed to tackle more complex projects.
FAQ
Here are some frequently asked questions:
- Can I use a different styling approach? Yes, you can use CSS-in-JS libraries like Styled Components, CSS modules, or a CSS framework like Tailwind CSS.
- How do I deploy this application? You can deploy your Next.js application to platforms like Vercel, Netlify, or AWS.
- How can I add more features? You can add features like drag-and-drop reordering, categories, and reminders.
- What is the best way to handle large datasets? For large datasets, consider using a database and server-side rendering or static site generation to optimize performance.
Building a to-do list app with Next.js is a fantastic way to learn the ropes of modern web development. You’ve now equipped yourself with the basics, and the path ahead is filled with possibilities. Continue to experiment, explore, and most importantly, enjoy the process of creating. With each line of code, you’re not just building an application; you’re building your skills and paving the way for more ambitious projects.
