Build a Simple Next.js Interactive To-Do List with Local Storage

In today’s fast-paced world, staying organized is key. A to-do list is a fundamental tool for managing tasks, boosting productivity, and keeping track of everything that needs to be done. While there are countless to-do list apps available, building your own offers a unique opportunity to learn and master web development concepts. This tutorial will guide you through creating a simple, interactive to-do list application using Next.js, a powerful React framework, and local storage to persist your tasks.

Why Build a To-Do List with Next.js?

Next.js provides several advantages for building web applications, especially those that require a good user experience and SEO optimization. Here’s why Next.js is an excellent choice for this project:

  • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows you to render your application on the server or generate static pages, improving SEO and initial load times.
  • Fast Refresh: Next.js offers a fast refresh feature, allowing you to see your changes instantly without losing the application state.
  • Built-in Routing: Next.js simplifies routing, making it easy to navigate between different pages in your application.
  • API Routes: Next.js provides a convenient way to create API endpoints within your application.
  • Optimized Performance: Next.js automatically optimizes images and code, resulting in faster and more efficient applications.

By building a to-do list with Next.js, you’ll gain practical experience with React components, state management, event handling, and local storage, all while creating a useful tool.

Prerequisites

Before you begin, ensure you have the following installed on your system:

  • Node.js and npm (or yarn): You’ll need Node.js and npm (Node Package Manager) or yarn to manage project dependencies.
  • A Code Editor: A code editor like Visual Studio Code, Sublime Text, or Atom will be helpful for writing and editing your code.
  • Basic knowledge of HTML, CSS, and JavaScript: Familiarity with these web technologies is essential for understanding the code and styling the application.

Step-by-Step Guide: Building Your To-Do List

Let’s dive into building your interactive to-do list application. Follow these steps to create a functional and user-friendly application.

1. Setting Up Your Next.js Project

First, create a new Next.js project using the following command in your terminal:

npx create-next-app my-todo-app
cd my-todo-app

This command creates a new Next.js project named “my-todo-app” and navigates you into the project directory.

2. Project Structure and Initial Setup

Your project directory should look like this:

my-todo-app/
├── node_modules/
├── pages/
│   └── _app.js
│   └── index.js
├── public/
├── .gitignore
├── next.config.js
├── package-lock.json
├── package.json
└── README.md

The `pages` directory is where you’ll create your application’s pages. The `index.js` file inside the `pages` directory serves as the home page of your application. Let’s start by cleaning up `index.js` and adding some basic HTML structure.

Open `pages/index.js` and replace its contents with the following code:

function HomePage() {
  return (
    <div>
      <h1>My To-Do List</h1>
      <!-- To-Do List components will go here -->
    </div>
  );
}

export default HomePage;

This code defines a functional React component named `HomePage` that renders a heading. Now, run your application with the following command in your terminal:

npm run dev

Open your browser and navigate to `http://localhost:3000`. You should see the “My To-Do List” heading.

3. Creating the To-Do Input Component

Next, let’s create a component for adding new tasks to your to-do list. Create a new file named `components/TodoInput.js` inside your project directory:

mkdir components
touch components/TodoInput.js

Add the following code to `components/TodoInput.js`:

import { useState } from 'react';

function TodoInput({ onAddTodo }) {
  const [text, setText] = useState('');

  const handleChange = (e) => {
    setText(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim() !== '') {
      onAddTodo(text.trim());
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        placeholder="Add a task"
      />
      <button type="submit">Add</button>
    </form>
  );
}

export default TodoInput;

This component includes:

  • State: Uses the `useState` hook to manage the input text.
  • Event Handlers: `handleChange` updates the input text, and `handleSubmit` adds the task to the list when the form is submitted.
  • Props: The `onAddTodo` prop is a function passed from the parent component to handle adding a new task.

4. Creating the To-Do List Component

Now, let’s create a component to display the to-do items. Create a new file named `components/TodoList.js` inside your project directory:

touch components/TodoList.js

Add the following code to `components/TodoList.js`:

function TodoList({ todos, onToggleComplete, onDeleteTodo }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => onToggleComplete(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => onDeleteTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

This component:

  • Receives an array of `todos` as a prop.
  • Maps through the `todos` array and renders each to-do item as a list item.
  • Includes a checkbox to mark tasks as complete, a text label, and a delete button.
  • Uses inline styling to apply a line-through effect to completed tasks.
  • The `onToggleComplete` and `onDeleteTodo` props are functions passed from the parent component to handle task completion and deletion.

5. Integrating the Components in the Main Page

Now, let’s integrate these components into our main page (`pages/index.js`). Update the `pages/index.js` file with the following code:

import { useState, useEffect } from 'react';
import TodoInput from '../components/TodoInput';
import TodoList from '../components/TodoList';

function HomePage() {
  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 todos change
  useEffect(() => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('todos', JSON.stringify(todos));
    }
  }, [todos]);

  const handleAddTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text: text,
      completed: false,
    };
    setTodos([...todos, newTodo]);
  };

  const handleToggleComplete = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleDeleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div style={{ margin: '20px' }}>
      <h1>My To-Do List</h1>
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList
        todos={todos}
        onToggleComplete={handleToggleComplete}
        onDeleteTodo={handleDeleteTodo}
      />
    </div>
  );
}

export default HomePage;

This updated code:

  • Imports the `TodoInput` and `TodoList` components.
  • Uses the `useState` hook to manage the `todos` array.
  • Implements the `handleAddTodo`, `handleToggleComplete`, and `handleDeleteTodo` functions to manage the to-do list.
  • Passes the `todos`, `handleToggleComplete`, and `handleDeleteTodo` functions as props to the `TodoList` component.
  • Passes the `handleAddTodo` function as a prop to the `TodoInput` component.
  • Uses `useEffect` hooks to load and save to-dos to local storage, ensuring data persistence across page reloads. The `typeof window !== ‘undefined’` check ensures that the code runs only in the browser and not during server-side rendering.

6. Adding Basic Styling

While the application is functional, it could use some styling to improve its appearance. You can add basic CSS to the `pages/_app.js` file or create a separate CSS file and import it. For simplicity, let’s add some inline styles to the components:

Modify the `pages/_app.js` file with the following:

import '../styles/global.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp;

Create a `styles` directory and add a `global.css` file with the following content:

body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
}

input[type="text"] {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
  font-size: 16px;
}

button {
  padding: 8px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

button:hover {
  background-color: #0056b3;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  padding: 10px;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
}

li:last-child {
  border-bottom: none;
}

input[type="checkbox"] {
  margin-right: 10px;
}

This CSS provides basic styling for the input field, buttons, and list items. You can customize the styles further to match your preferences.

7. Testing and Using Your To-Do List

With the code in place, run your Next.js application using `npm run dev`. Open your browser and navigate to `http://localhost:3000`. You should see your to-do list application with the input field and the list of to-do items.

Try the following to test the application:

  • Enter a task in the input field and click the “Add” button. The task should be added to the list.
  • Click the checkbox next to a task to mark it as complete. The text should be crossed out.
  • Click the “Delete” button to remove a task from the list.
  • Refresh the page. Your to-do list should persist thanks to local storage.

Common Mistakes and How to Fix Them

As you build this to-do list application, you might encounter some common mistakes. Here are a few and how to resolve them:

1. Not Saving Data to Local Storage

Mistake: The to-do list items are not persisting after a page refresh.

Fix: Make sure you’re using the `useEffect` hook to save and load data from local storage. The `useEffect` hook with an empty dependency array (`[]`) will run only once when the component mounts. The `useEffect` hook with `[todos]` will run whenever `todos` changes. Also, ensure you are stringifying your data using `JSON.stringify()` when saving to local storage and parsing it using `JSON.parse()` when retrieving it.

2. Incorrect State Updates

Mistake: Tasks are not being added, marked as complete, or deleted correctly.

Fix: Double-check your state update logic. When adding a new task, make sure you’re creating a new object with a unique ID and appending it to the existing `todos` array. When marking a task as complete, use the `map` method to create a new array with the updated task. When deleting a task, use the `filter` method to create a new array without the deleted task. Avoid mutating the state directly; always create new arrays or objects.

3. Missing Dependencies in useEffect

Mistake: The `useEffect` hook doesn’t re-run when a value it depends on changes, leading to unexpected behavior.

Fix: Carefully review the dependencies array in the `useEffect` hook. If a variable used inside the effect changes, you need to include it in the dependency array. For example, if your effect depends on the `todos` state, include `[todos]` in the dependency array. If you are using server-side rendering, you should check for the availability of the `window` object before accessing `localStorage`.

4. Using Inline Styles Extensively

Mistake: The code becomes harder to read and maintain.

Fix: While inline styles are useful for quick adjustments, create a separate CSS file or use a CSS-in-JS solution (like styled-components) for better organization and reusability.

5. Not Handling Empty Input

Mistake: Users can add empty tasks to the list.

Fix: Add a check in the `handleSubmit` function to prevent adding empty tasks. Trim the input text using `.trim()` and only add the task if it’s not an empty string.

Key Takeaways and Next Steps

Congratulations! You’ve successfully built a simple, interactive to-do list application using Next.js and local storage. Here are the key takeaways from this project:

  • Component-Based Architecture: You’ve learned how to create and organize your application using React components.
  • State Management: You’ve used the `useState` hook to manage the state of your application.
  • Event Handling: You’ve learned how to handle user interactions, such as form submissions and button clicks.
  • Local Storage: You’ve used local storage to persist data across page reloads.
  • Next.js Fundamentals: You’ve become familiar with the basics of Next.js, including setting up a project, routing, and adding styling.

To further enhance your skills, consider the following next steps:

  • Implement Advanced Features: Add features such as task prioritization, due dates, categories, and the ability to edit tasks.
  • Use a Database: Instead of local storage, connect your application to a database (e.g., MongoDB, PostgreSQL) to store your to-do items.
  • Add Authentication: Implement user authentication to allow multiple users to manage their to-do lists.
  • Deploy Your Application: Deploy your application to a hosting platform like Vercel or Netlify.
  • Explore Advanced Next.js Features: Learn about server-side rendering, API routes, and other advanced Next.js features to build more complex applications.

This project serves as a solid foundation for your web development journey. Keep practicing, experimenting, and exploring new concepts to become a proficient web developer. The more you build, the more you learn, and the more confident you’ll become in your abilities.