Build a Simple Next.js Interactive Task Management App

Written by

in

In the fast-paced world of web development, staying organized and managing tasks effectively is crucial. Whether you’re a seasoned developer juggling multiple projects or a beginner learning the ropes, a well-designed task management application can be a game-changer. This article will guide you through building a simple, yet functional, interactive task management app using Next.js. We’ll cover everything from setting up your development environment to implementing core features like adding, listing, and completing tasks. By the end of this tutorial, you’ll not only have a practical application to help you manage your own tasks, but you’ll also gain a solid understanding of fundamental Next.js concepts and how to apply them in a real-world scenario.

Why Build a Task Management App?

Task management apps are ubiquitous for a reason: they solve a fundamental problem – keeping track of what needs to be done. They provide a centralized location to list, prioritize, and monitor tasks, leading to increased productivity and reduced stress. Building one yourself, especially with a framework like Next.js, offers several advantages:

  • Learning Opportunity: It’s a fantastic way to learn and practice essential web development skills, including state management, component composition, and API interactions.
  • Customization: You have complete control over the features and design, allowing you to tailor the app to your specific needs.
  • Practical Application: You’ll create something useful that you can use daily to manage your own tasks.

This tutorial is designed for beginners and intermediate developers. We’ll break down complex concepts into manageable chunks, providing clear explanations and code examples. Let’s get started!

Setting Up Your Development Environment

Before we dive into the code, let’s make sure you have the necessary tools installed and configured. You’ll need:

  • Node.js and npm (or yarn): Node.js is a JavaScript runtime environment, and npm (Node Package Manager) or yarn is used to manage project dependencies. You can download them from the official Node.js website.
  • A Code Editor: Choose a code editor you’re comfortable with, such as Visual Studio Code, Sublime Text, or Atom.
  • A Terminal: You’ll need a terminal (command prompt or terminal) to run commands and manage your project.

Once you have these prerequisites, you can create a new Next.js project using the following command in your terminal:

npx create-next-app task-management-app

This command will create a new directory called `task-management-app` with the basic structure of a Next.js project. Navigate into the project directory:

cd task-management-app

Now, start the development server:

npm run dev

Open your web browser and go to `http://localhost:3000`. You should see the default Next.js welcome page. Congratulations, your development environment is set up!

Project Structure and Basic Components

Let’s take a look at the basic project structure that `create-next-app` provides:

  • `pages/` directory: This is where you’ll create your pages. Each file in this directory represents a route in your application. For example, `pages/index.js` corresponds to the root route (`/`).
  • `components/` directory: This is where you’ll store reusable UI components.
  • `public/` directory: This directory holds static assets like images, fonts, and other files.
  • `styles/` directory: This is where you’ll keep your CSS or other styling files.
  • `package.json`: This file contains information about your project, including dependencies and scripts.

For our task management app, we’ll start by creating the following components:

  • `TaskInput.js`: A component for adding new tasks.
  • `TaskList.js`: A component to display the list of tasks.
  • `TaskItem.js`: A component to represent a single task in the list.

Let’s create these components in the `components/` directory. Create a file named `components/TaskInput.js` and add the following code:

import React, { useState } from 'react';

function TaskInput({ addTask }) {
 const [taskText, setTaskText] = useState('');

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

 return (
  <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
  <input
  type="text"
  value={taskText}
  onChange={(e) => setTaskText(e.target.value)}
  placeholder="Add a task..."
  style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
  /
  >
  <button type="submit" style={{ padding: '8px 15px', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
  Add Task
  </button>
  </form>
 );
}

export default TaskInput;

This component includes a text input and a button. When the user types a task and clicks the button, the `handleSubmit` function is called, which calls the `addTask` function passed from the parent component. It also clears the input field after a task is added.

Next, create `components/TaskList.js`:

import React from 'react';
import TaskItem from './TaskItem';

function TaskList({ tasks, toggleComplete, deleteTask }) {
 return (
  <ul style={{ listStyle: 'none', padding: 0 }}>
  {tasks.map((task) => (
  <TaskItem
  key={task.id}
  task={task}
  toggleComplete={toggleComplete}
  deleteTask={deleteTask}
  /
  >
  ))}
  </ul>
 );
}

export default TaskList;

This component receives an array of tasks and renders a `TaskItem` component for each task. It also receives the `toggleComplete` and `deleteTask` functions, which are passed to the `TaskItem` component.

Now, create `components/TaskItem.js`:

import React from 'react';

function TaskItem({ task, toggleComplete, deleteTask }) {
 return (
  <li style={{ display: 'flex', alignItems: 'center', marginBottom: '10px' }}>
  <input
  type="checkbox"
  checked={task.completed}
  onChange={() => toggleComplete(task.id)}
  style={{ marginRight: '10px' }}
  /
  >
  <span style={{ textDecoration: task.completed ? 'line-through' : 'none', flexGrow: 1 }}>
  {task.text}
  </span>
  <button
  onClick={() => deleteTask(task.id)}
  style={{ backgroundColor: '#f44336', color: 'white', border: 'none', padding: '5px 10px', borderRadius: '4px', cursor: 'pointer' }}
  >
  Delete
  </button>
  </li>
 );
}

export default TaskItem;

This component represents a single task. It displays the task text, a checkbox to mark the task as completed, and a delete button. It receives the `task`, `toggleComplete`, and `deleteTask` props from the `TaskList` component.

Implementing Task Management Logic

Now that we have our basic components, let’s implement the core task management logic in our `pages/index.js` file. This is where we’ll manage the state of our tasks and handle user interactions.

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

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

function HomePage() {
 const [tasks, setTasks] = useState([]);

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

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

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

 return (
  <div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
  <h2 style={{ textAlign: 'center', marginBottom: '20px' }}>Task Management App</h2>
  <TaskInput addTask={addTask} /
  >
  <TaskList tasks={tasks} toggleComplete={toggleComplete} deleteTask={deleteTask} /
  >
  </div>
 );
}

export default HomePage;

Let’s break down this code:

  • State Management: We use the `useState` hook to manage the `tasks` state. This state is an array of task objects.
  • `addTask` Function: This function adds a new task to the `tasks` array. It creates a new task object with a unique ID (using `Date.now()`), the task text, and a `completed` status set to `false`.
  • `toggleComplete` Function: This function toggles the `completed` status of a task with the given ID. It uses the `map` method to iterate over the `tasks` array and update the appropriate task.
  • `deleteTask` Function: This function removes a task from the `tasks` array. It uses the `filter` method to create a new array containing only the tasks that do not match the given ID.
  • Component Composition: We import and use the `TaskInput` and `TaskList` components, passing the necessary props to them.

With this code, you should now be able to add tasks, mark them as complete, and delete them. Test your application in the browser. Add some tasks, check them off, and delete them to ensure everything is working as expected.

Styling Your App

While the app is functional, it could use some styling to improve its appearance. We can use CSS-in-JS or create a CSS file. For simplicity, let’s use inline styles for this tutorial. However, in a real-world project, you’d likely use a CSS-in-JS solution (like styled-components or Emotion) or a CSS preprocessor (like Sass or Less) for better organization and maintainability.

We’ve already added some basic styling to the components. You can further customize the appearance by modifying the `style` attributes in the components. For example, you can add padding, margins, colors, and fonts to create a more visually appealing interface. Experiment with different styles to see how they affect the app’s look and feel.

Here are some styling suggestions to get you started:

  • Colors: Choose a color scheme that you like. Use different colors for completed and incomplete tasks.
  • Fonts: Experiment with different fonts to improve readability.
  • Layout: Use margins, padding, and flexbox to create a clear and organized layout.
  • Responsiveness: Ensure that your app looks good on different screen sizes.

Adding Local Storage for Persistence

Currently, the tasks are lost when you refresh the page. To make the task list persistent, we can use local storage. This allows us to save the tasks in the user’s browser, so they are available even after the page is reloaded.

Here’s how to implement local storage:

  1. Load tasks from local storage on component mount: When the component mounts, check if there are any tasks stored in local storage. If there are, load them into the `tasks` state.
  2. Save tasks to local storage whenever the tasks state changes: Use the `useEffect` hook to save the `tasks` state to local storage whenever it changes.

Modify `pages/index.js` as follows:

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

function HomePage() {
 const [tasks, setTasks] = useState([]);

 // Load tasks from local storage on component mount
 useEffect(() => {
 const storedTasks = localStorage.getItem('tasks');
 if (storedTasks) {
  setTasks(JSON.parse(storedTasks));
 }
 }, []);

 // Save tasks to local storage whenever the tasks state changes
 useEffect(() => {
 localStorage.setItem('tasks', JSON.stringify(tasks));
 }, [tasks]);

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

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

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

 return (
  <div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
  <h2 style={{ textAlign: 'center', marginBottom: '20px' }}>Task Management App</h2>
  <TaskInput addTask={addTask} /
  >
  <TaskList tasks={tasks} toggleComplete={toggleComplete} deleteTask={deleteTask} /
  >
  </div>
 );
}

export default HomePage;

Let’s break down the changes:

  • Import `useEffect`: We import the `useEffect` hook from React.
  • Loading Tasks: The first `useEffect` hook runs when the component mounts (i.e., when the page loads). It checks if there are any tasks stored in local storage under the key “tasks.” If there are, it parses the JSON string and sets the `tasks` state.
  • Saving Tasks: The second `useEffect` hook runs whenever the `tasks` state changes. It stringifies the `tasks` array using `JSON.stringify()` and saves it to local storage under the key “tasks.” The dependency array `[tasks]` ensures that this effect runs whenever the `tasks` state changes.

Now, your tasks will be saved in your browser’s local storage and will persist even after you refresh the page.

Common Mistakes and How to Fix Them

As you build your task management app, you might encounter some common mistakes. Here are a few and how to address them:

  • Incorrect State Updates: When updating state, it’s crucial to correctly update the state object. For example, when toggling the `completed` status of a task, ensure you are creating a new object and not directly mutating the existing one. Always use the spread operator (`…`) to create new arrays and objects when updating state.
  • Missing Dependencies in `useEffect`: When using `useEffect`, make sure to include all dependencies in the dependency array. Failing to do so can lead to unexpected behavior or infinite loops. For example, if you are saving tasks to local storage using `useEffect`, include the `tasks` state in the dependency array.
  • Incorrect Key Prop in `map()`: When rendering a list of items using `map()`, always provide a unique `key` prop to each element. This helps React efficiently update the DOM. In our example, we used `task.id` as the key.
  • Not Handling Empty Input: Make sure to handle empty input fields in the `TaskInput` component. Prevent adding tasks with empty text by checking if the input is not empty before adding the task.
  • Forgetting to Import Components: Always double-check that you have imported all the necessary components correctly.

Advanced Features and Next Steps

This tutorial provides a solid foundation for building a task management app. Here are some advanced features and next steps to consider:

  • Task Prioritization: Add the ability to prioritize tasks (e.g., high, medium, low).
  • Due Dates: Include due dates for each task.
  • Task Categories/Tags: Allow users to categorize or tag tasks.
  • User Authentication: Implement user authentication to allow multiple users to use the app.
  • Backend Integration: Integrate the app with a backend database (e.g., using Firebase, MongoDB, or a custom API) to store tasks persistently and allow access from multiple devices.
  • Advanced Styling: Use a CSS-in-JS library or a CSS preprocessor for more advanced styling and theming.
  • Testing: Write unit and integration tests to ensure the app functions correctly.
  • Deployment: Deploy your app to a hosting platform like Vercel or Netlify.

Key Takeaways

  • Next.js Fundamentals: You’ve learned about Next.js project structure, component composition, state management, and the `useEffect` hook.
  • Building a UI: You’ve created interactive UI components for adding, listing, and completing tasks.
  • Local Storage: You’ve implemented local storage to persist tasks.
  • Problem-Solving: You’ve tackled common mistakes and learned how to debug and improve your code.

By building this task management app, you’ve gained practical experience with Next.js and web development principles. This project serves as an excellent starting point for further exploration and experimentation with more complex features and functionalities. Remember to practice, experiment, and don’t be afraid to try new things. The more you code, the better you’ll become.