Build a Simple Next.js Interactive Todo List App

In today’s fast-paced digital world, managing tasks efficiently is more crucial than ever. From personal errands to complex project management, a well-designed to-do list application can be a game-changer. This article provides a comprehensive guide to building a simple, yet functional, interactive to-do list app using Next.js, a powerful React framework for building modern web applications. Whether you’re a beginner just starting your journey in web development or an intermediate developer looking to hone your skills, this tutorial will equip you with the knowledge to create a practical and user-friendly to-do list application.

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

Next.js offers several advantages for this project. Its server-side rendering (SSR) capabilities improve SEO and initial load times. Its built-in routing simplifies navigation, and its support for static site generation (SSG) allows for optimal performance. Furthermore, Next.js’s developer-friendly features, such as hot module replacement (HMR) and easy deployment, make the development process smooth and efficient. For a to-do list app, these features translate into a faster, more responsive, and more easily maintainable application.

Prerequisites

Before we dive in, ensure you have the following installed on your system:

  • Node.js and npm (Node Package Manager) or yarn installed.
  • A code editor (e.g., VS Code, Sublime Text, Atom).
  • Basic knowledge of HTML, CSS, and JavaScript.
  • Familiarity with React components and JSX.

Setting Up Your Next.js 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 the cd command.

Project Structure

Next.js automatically sets up a basic project structure. Here’s a brief overview:

  • pages/: This directory contains your application’s pages. Each file in this directory represents a route.
  • public/: This directory is for static assets like images, fonts, and other files.
  • styles/: This directory is for your CSS stylesheets.
  • package.json: This file contains project dependencies and scripts.

Building the To-Do List Components

We’ll create several components to build our to-do list app. First, let’s create a components directory in the root of your project to organize our components. Inside this directory, we’ll create the following components:

  • TodoItem.js: Represents a single to-do item.
  • TodoList.js: Renders the list of to-do items.
  • TodoForm.js: Handles the input for adding new to-do items.

TodoItem.js

This component will display a single to-do item. Create a file named TodoItem.js inside the components directory with the following code:

// components/TodoItem.js
import React from 'react';

const TodoItem = ({ todo, onToggleComplete, onDeleteTodo }) => {
  return (
    <li className="todo-item">
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onToggleComplete(todo.id)}
        />
        <span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
      </label>
      <button onClick={() => onDeleteTodo(todo.id)}>Delete</button>
    </li>
  );
};

export default TodoItem;

This component receives a todo object as a prop, which includes the item’s id, text, and completed status. It renders a checkbox to mark the item as complete, the to-do item’s text, and a delete button. We also use conditional styling to apply a ‘completed’ class if the item is marked as complete.

TodoList.js

This component will render the list of to-do items. Create a file named TodoList.js inside the components directory with the following code:

// components/TodoList.js
import React from 'react';
import TodoItem from './TodoItem';

const TodoList = ({ todos, onToggleComplete, onDeleteTodo }) => {
  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggleComplete={onToggleComplete}
          onDeleteTodo={onDeleteTodo}
        />
      ))}
    </ul>
  );
};

export default TodoList;

This component takes an array of todos as a prop and maps over it, rendering a TodoItem component for each to-do item. It also passes the onToggleComplete and onDeleteTodo functions to each TodoItem.

TodoForm.js

This component will handle the form for adding new to-do items. Create a file named TodoForm.js inside the components directory with the following code:

// components/TodoForm.js
import React, { useState } from 'react';

const TodoForm = ({ onAddTodo }) => {
  const [text, setText] = useState('');

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

  return (
    <form onSubmit={handleSubmit} className="todo-form">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo..."
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default TodoForm;

This component uses a useState hook to manage the input field’s value. When the form is submitted, it calls the onAddTodo function (passed as a prop) with the input text, and then clears the input field.

Implementing the Main Page (pages/index.js)

Now, let’s create the main page of our application. Open pages/index.js and replace the existing code with the following:

// pages/index.js
import React, { useState, useEffect } from 'react';
import TodoList from '../components/TodoList';
import TodoForm from '../components/TodoForm';

const Home = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    // Load todos from localStorage on component mount
    const storedTodos = localStorage.getItem('todos');
    if (storedTodos) {
      setTodos(JSON.parse(storedTodos));
    }
  }, []);

  useEffect(() => {
    // Save todos to localStorage whenever todos change
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      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 className="container">
      <h1>My To-Do List</h1>
      <TodoForm onAddTodo={addTodo} />
      <TodoList
        todos={todos}
        onToggleComplete={toggleComplete}
        onDeleteTodo={deleteTodo}
      />
    </div>
  );
};

export default Home;

This code defines the main page component. It uses useState to manage the todos array. The useEffect hook is used to load todos from localStorage when the component mounts and to save todos to localStorage whenever the todos state changes. It also includes functions to add, toggle completion status, and delete to-do items. The component renders the TodoForm and TodoList components, passing the necessary props.

Adding Styles (styles/globals.css)

To make the application visually appealing, let’s add some basic styles. Open styles/globals.css and add the following CSS:

/* styles/globals.css */
body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
}

.container {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

.todo-form {
  display: flex;
  margin-bottom: 20px;
}

.todo-form input {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
}

.todo-form button {
  padding: 10px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-list {
  list-style: none;
  padding: 0;
}

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

.todo-item label {
  display: flex;
  align-items: center;
  flex-grow: 1;
}

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

.todo-item button {
  padding: 5px 10px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 10px;
}

.completed {
  text-decoration: line-through;
  color: #888;
}

This CSS provides basic styling for the components, including the container, heading, form, and to-do items. It also styles the completed items with a line-through effect.

Running Your Application

Now that we’ve built the components and added styles, let’s run the application. In your terminal, run the following command:

npm run dev

This will start the development server. Open your browser and go to http://localhost:3000 to see your to-do list app in action.

Common Mistakes and How to Fix Them

1. Incorrect Import Paths

One common mistake is using incorrect import paths for your components. Make sure your import paths are relative to the file you’re importing from. For example, if you’re importing TodoItem.js from TodoList.js, and both are in the same directory, the import statement should be import TodoItem from './TodoItem';.

2. Not Handling State Updates Correctly

When updating state, it’s crucial to correctly update the state using the setTodos function. Always use the previous state to update the new state. For example, when adding a new todo, use the spread operator (...) to add the new item to the existing array: setTodos([...todos, newTodo]);

3. Forgetting to Bind Event Handlers

When passing functions as props to child components, ensure that you correctly bind the event handlers to the component instance if you’re using class components. In functional components, this is handled automatically.

4. Styling Issues

Ensure that you have correctly linked your CSS file in the _app.js file (or where your global styles are imported). Check for any typos or CSS specificity issues that might be affecting your styles. Use your browser’s developer tools to inspect the elements and see if the styles are being applied.

Enhancements and Next Steps

This is a basic to-do list app, but it can be enhanced in several ways:

  • Adding Due Dates: Implement a date picker to assign due dates to each to-do item.
  • Implementing Categories: Allow users to categorize their tasks (e.g., Work, Personal, etc.).
  • Adding Drag and Drop: Use a library like react-beautiful-dnd to enable drag-and-drop functionality for reordering tasks.
  • Connecting to a Database: Store the to-do items in a database (e.g., Firebase, MongoDB) to persist the data.
  • Adding Authentication: Implement user authentication to allow multiple users to use the app.

Key Takeaways

This tutorial has provided a solid foundation for building an interactive to-do list application with Next.js. You’ve learned how to create components, manage state, handle user input, and apply styles. The use of useEffect for loading and saving data to local storage ensures that your to-do items persist across sessions. By understanding the core concepts and following the steps outlined in this article, you can build upon this foundation and create more complex and feature-rich web applications. This project not only teaches the basics of Next.js, but also reinforces fundamental web development concepts such as component design, state management, and user interface interactions.

The journey of building this simple to-do list app using Next.js is a testament to the power of breaking down complex problems into manageable parts. Each component, from the TodoItem to the TodoForm, plays a crucial role in the overall functionality. The state management, handled using the useState and useEffect hooks, ensures that the application remains responsive and the data persists. As you continue to build and experiment with Next.js, remember that the most important aspect is the iterative process of learning, building, and refining. Embrace the challenges, learn from the mistakes, and most importantly, enjoy the process of bringing your ideas to life.