Build a Simple Next.js Interactive Task Manager App

Written by

in

In today’s fast-paced world, staying organized is key to productivity. Whether you’re a student juggling assignments, a professional managing projects, or simply someone trying to keep track of daily chores, a task manager can be an invaluable tool. But what if you could build your own, tailored to your specific needs? This article will guide you through creating a simple, yet functional, interactive task manager app using Next.js, a powerful React framework.

Why Build a Task Manager with Next.js?

Next.js offers several advantages that make it an excellent choice for this project:

  • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows you to pre-render your application’s pages, improving SEO and initial load times.
  • React-Based: If you’re familiar with React, you’ll feel right at home with Next.js. It leverages the React ecosystem and component-based architecture.
  • Built-in Routing: Next.js simplifies routing with its file-system-based routing, making navigation straightforward.
  • API Routes: Easily create API endpoints within your Next.js application for handling data and interacting with backend services.
  • Developer Experience: Next.js provides a great developer experience with features like hot module replacement (HMR) and automatic code splitting.

By building a task manager with Next.js, you’ll not only learn how to create a useful application but also gain valuable experience with a modern web development framework.

Project Setup and Prerequisites

Before we dive in, let’s make sure you have the necessary tools installed:

  • Node.js and npm (or yarn): Make sure you have Node.js (version 14 or higher) and npm (or yarn) installed on your system. You can download them from nodejs.org.
  • Text Editor or IDE: Choose your preferred code editor or IDE, such as VS Code, Sublime Text, or WebStorm.
  • Basic JavaScript and React Knowledge: A foundational understanding of JavaScript and React concepts will be helpful.

Now, let’s set up our Next.js project:

  1. Create a new Next.js project: Open your terminal and run the following command:
    npx create-next-app task-manager-app

    This command will create a new Next.js project with the name “task-manager-app”.

  2. Navigate to your project directory:
    cd task-manager-app
  3. Start the development server:
    npm run dev

    This will start the development server, and you can access your app in your browser at http://localhost:3000.

Building the Task Manager Components

Our task manager will consist of several key components:

  • Task Input: A form to add new tasks.
  • Task List: Displays the list of tasks.
  • Task Item: Represents an individual task, allowing users to mark it as complete or delete it.

1. Task Input Component

Create a new file named components/TaskInput.js. This component will handle the input for adding new tasks.

import { useState } from 'react';

const TaskInput = ({ onAddTask }) => {
  const [taskText, setTaskText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (taskText.trim() !== '') {
      onAddTask(taskText);
      setTaskText('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="mb-4">
      <input
        type="text"
        value={taskText}
        onChange={(e) => setTaskText(e.target.value)}
        placeholder="Add a task..."
        className="border rounded py-2 px-3 mr-2 w-2/3"
      />
      <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
        Add
      </button>
    </form>
  );
};

export default TaskInput;

This component uses the useState hook to manage the input field’s value. The handleSubmit function prevents the default form submission behavior, calls the onAddTask prop (which we’ll define later), and clears the input field after a task is added.

2. Task Item Component

Create a new file named components/TaskItem.js. This component represents a single task in the list.


const TaskItem = ({ task, onDelete, onComplete }) => {
  return (
    <li className="flex items-center justify-between py-2 border-b last:border-none">
      <span className={`flex-grow ${task.completed ? 'line-through text-gray-500' : ''}`}>
        {task.text}
      </span>
      <div>
        <button
          onClick={() => onComplete(task.id)}
          className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-2 rounded mr-2"
        >
          {task.completed ? 'Undo' : 'Complete'}
        </button>
        <button
          onClick={() => onDelete(task.id)}
          className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded"
        </button>
      </div>
    </li>
  );
};

export default TaskItem;

This component displays the task text and provides buttons to mark the task as complete or delete it. The task.completed property determines whether the task text is struck through.

3. Task List Component

Create a new file named components/TaskList.js. This component will display the list of tasks, using the TaskItem component for each task.


import TaskItem from './TaskItem';

const TaskList = ({ tasks, onDeleteTask, onCompleteTask }) => {
  return (
    <ul>
      {tasks.map((task) => (
        <TaskItem
          key={task.id}
          task={task}
          onDelete={onDeleteTask}
          onComplete={onCompleteTask}
        />
      ))}
    </ul>
  );
};

export default TaskList;

This component receives an array of tasks and renders a TaskItem component for each task in the array. It also passes the onDeleteTask and onCompleteTask functions to the TaskItem component.

4. Integrating the Components in the Page

Now, let’s integrate these components into our main page (pages/index.js). Replace the existing content with the following:


import { useState } from 'react';
import TaskInput from '../components/TaskInput';
import TaskList from '../components/TaskList';

const Home = () => {
  const [tasks, setTasks] = useState([]);

  const addTask = (text) => {
    const newTask = {
      id: Date.now(), // Simple unique ID
      text,
      completed: false,
    };
    setTasks([...tasks, newTask]);
  };

  const deleteTask = (id) => {
    setTasks(tasks.filter((task) => task.id !== id));
  };

  const completeTask = (id) => {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Task Manager</h1>
      <TaskInput onAddTask={addTask} />
      <TaskList
        tasks={tasks}
        onDeleteTask={deleteTask}
        onCompleteTask={completeTask}
      />
    </div>
  );
};

export default Home;

In this code:

  • We import the components we created.
  • We use the useState hook to manage the list of tasks.
  • The addTask function adds a new task to the list.
  • The deleteTask function removes a task.
  • The completeTask function toggles the completed status of a task.
  • We render the TaskInput and TaskList components, passing the necessary props.

Adding Styling with Tailwind CSS

To make our task manager look visually appealing, we’ll use Tailwind CSS, a utility-first CSS framework. You can easily integrate Tailwind CSS into your Next.js project.

  1. Install Tailwind CSS and its dependencies:
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
  2. Configure your template paths: In your tailwind.config.js file, update the content array to include the paths to all of your template files.
    /** @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: {
          // ...
        },
      },
      plugins: [],
    }
    
  3. Import Tailwind’s styles: In your styles/globals.css file, add the following directives at the top:
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

Now you can use Tailwind CSS classes in your components. Refer to the Tailwind CSS documentation for a comprehensive list of classes: tailwindcss.com/docs/installation. The code snippets above already include Tailwind CSS classes for basic styling.

Implementing Task Persistence (Optional)

Currently, our tasks are only stored in the browser’s memory and will be lost when you refresh the page. To make the task manager more useful, let’s add persistence using localStorage. This will allow us to save the tasks in the user’s browser, so they persist between sessions.

Modify the pages/index.js file as follows:


import { useState, useEffect } from 'react';
import TaskInput from '../components/TaskInput';
import TaskList from '../components/TaskList';

const Home = () => {
  const [tasks, setTasks] = useState(() => {
    // Load tasks from localStorage on component mount
    if (typeof window !== 'undefined') {
      const savedTasks = localStorage.getItem('tasks');
      return savedTasks ? JSON.parse(savedTasks) : [];
    }
    return [];
  });

  useEffect(() => {
    // Save tasks to localStorage whenever the tasks state changes
    if (typeof window !== 'undefined') {
      localStorage.setItem('tasks', JSON.stringify(tasks));
    }
  }, [tasks]);

  const addTask = (text) => {
    const newTask = {
      id: Date.now(), // Simple unique ID
      text,
      completed: false,
    };
    setTasks([...tasks, newTask]);
  };

  const deleteTask = (id) => {
    setTasks(tasks.filter((task) => task.id !== id));
  };

  const completeTask = (id) => {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Task Manager</h1>
      <TaskInput onAddTask={addTask} />
      <TaskList
        tasks={tasks}
        onDeleteTask={deleteTask}
        onCompleteTask={completeTask}
      />
    </div>
  );
};

export default Home;

Here’s how this code works:

  • Loading Tasks: The useState hook now accepts a function as its initial value. This function checks if the code is running in a browser environment (typeof window !== 'undefined'). If so, it attempts to retrieve tasks from localStorage. If tasks exist, they are parsed from JSON; otherwise, an empty array is used as the initial state.
  • Saving Tasks: The useEffect hook is used to save the tasks to localStorage whenever the tasks state changes. It also checks if the code is running in a browser environment before attempting to access localStorage. The [tasks] dependency ensures that this effect runs whenever the tasks array is updated.

With these changes, your tasks will now be saved in the user’s browser and will persist even when they close the tab or refresh the page.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners might encounter and how to address them:

  • Incorrect Component Imports: Ensure that you import components correctly. Double-check the file paths in your import statements. For example:
    import TaskInput from '../components/TaskInput';
  • Unintended State Updates: When updating state, make sure you’re creating a new array or object instead of directly modifying the existing one. For example, when adding a task, use the spread operator (...) to create a new array.
    setTasks([...tasks, newTask]);
  • Missing Dependencies in useEffect: If you’re using useEffect, make sure you include all the dependencies (variables that are used inside the effect) in the dependency array. This prevents unexpected behavior and ensures the effect runs when necessary.
  • Incorrect Tailwind CSS Application: Make sure you have installed and configured Tailwind CSS correctly. Also, verify that you are using the correct class names from the Tailwind CSS documentation and that the CSS is being applied to the HTML elements. Check your browser’s developer tools to see if the styles are being applied.
  • Issues with `localStorage`: Remember that `localStorage` is only available in a browser environment. If you try to access it during server-side rendering (SSR), you’ll encounter an error. To avoid this, check if `window` is defined before accessing `localStorage`.

Key Takeaways and Summary

In this tutorial, we’ve successfully built a simple, interactive task manager app using Next.js. We covered the essential components, including the task input, task list, and task item, and implemented features like adding, deleting, and completing tasks. We also explored how to add styling with Tailwind CSS and how to persist data using localStorage.

By following these steps, you’ve gained practical experience with Next.js, React, and fundamental web development concepts. You should now be able to:

  • Set up a Next.js project.
  • Create and structure React components.
  • Manage state using the useState hook.
  • Handle user input and events.
  • Render dynamic lists of data.
  • Apply styling with Tailwind CSS.
  • Persist data using localStorage.

Optional FAQ

Here are some frequently asked questions about this project:

Q: How can I deploy this app?

A: You can deploy your Next.js app to platforms like Vercel (recommended, as it’s the easiest for Next.js apps), Netlify, or other hosting providers. Vercel has excellent integration with Next.js and provides automatic deployments from your Git repository.

Q: Can I add more features to this task manager?

A: Absolutely! You can extend this app with features like:

  • Task prioritization
  • Due dates
  • Task categories/tags
  • User authentication
  • Integration with a backend database

Q: Why use Tailwind CSS?

A: Tailwind CSS offers a utility-first approach to styling. It provides pre-defined CSS classes that can be applied directly to your HTML elements, allowing for rapid development and customization. It also helps maintain consistency in your design.

Q: What are some alternatives to localStorage for data persistence?

A: For more complex applications, you might consider using:

  • Cookies: Small pieces of data stored on the user’s computer by the website.
  • IndexedDB: A low-level API for client-side storage of significant amounts of structured data.
  • Backend Databases: For persistent storage and more complex data management, you’d typically use a backend database (e.g., PostgreSQL, MongoDB) and an API to interact with it.

Q: How do I handle errors in this application?

A: Error handling can be implemented in a few ways. You can add try-catch blocks around potentially problematic code (e.g., when interacting with localStorage). You could also add error boundaries to your React components to gracefully handle errors during rendering. Furthermore, consider adding error messages to the UI to provide feedback to the user and logging errors to a service like Sentry or Rollbar to monitor and debug your application.

Building this task manager is just the beginning. The skills and knowledge you’ve gained can be applied to many other web development projects. Continue exploring, experimenting, and building to further enhance your skills. The world of web development is constantly evolving, so embrace the learning process, and don’t be afraid to try new things. Keep practicing, and you’ll be amazed at what you can create!