Skip to content

App Integration: Fetching and Displaying Tasks

Now, let's put these concepts into practice by refactoring our Task Management App. We will:

  1. Create a mock API route to simulate fetching tasks.
  2. Update our app/dashboard/page.tsx (a Server Component) to fetch data from this mock API.
  3. Modify our components/TaskList.tsx to receive tasks as a prop, as it will no longer be responsible for fetching its own initial data.
  4. Implement a loading UI for our dashboard.

1. Create a Mock API Route (app/api/tasks/route.ts)

Let's create a simple API endpoint that will return a hardcoded list of tasks. In a real application, this would connect to a database.

  1. Create new folders and a file for your API route:

    my-task-app/
    └── app/
        ├── ...
        └── api/                    <-- NEW DIRECTORY
            └── tasks/              <-- NEW DIRECTORY
                └── route.ts        <-- NEW FILE
    
  2. Add the following content to app/api/tasks/route.ts. This file defines a Route Handler, which is equivalent to creating an API endpoint.

    app/api/tasks/route.ts

    // app/api/tasks/route.ts
    // This file defines a Next.js Route Handler for the /api/tasks endpoint.
    // It simulates fetching tasks from a backend.
    
    import { NextResponse } from 'next/server';
    
    // Define a type for our mock task data
    interface MockTask {
      id: string;
      title: string;
      description: string;
      completed: boolean;
      priority: 'Low' | 'Medium' | 'High';
      dueDate: string; // YYYY-MM-DD format
    }
    
    // Simulate a delay to demonstrate loading states
    const simulateDelay = (ms: number) => new Promise(res => setTimeout(res, ms));
    
    // Hardcoded mock task data
    const mockTasks: MockTask[] = [
      {
        id: 'task-1',
        title: 'Complete Module 5 of Next.js Course',
        description: 'Understand Server Components, SSR, SSG, and implement data fetching.',
        completed: false,
        priority: 'High',
        dueDate: '2025-06-15',
      },
      {
        id: 'task-2',
        title: 'Refactor TaskList to accept props',
        description: 'Remove internal state for initial task loading and pass tasks via props.',
        completed: false,
        priority: 'High',
        dueDate: '2025-06-12',
      },
      {
        id: 'task-3',
        title: 'Set up Next.js authentication',
        description: 'Prepare for integrating Firebase Authentication in Module 8.',
        completed: false,
        priority: 'Medium',
        dueDate: '2025-06-20',
      },
      {
        id: 'task-4',
        title: 'Buy groceries',
        description: 'Milk, eggs, bread, vegetables, fruits.',
        completed: true,
        priority: 'Low',
        dueDate: '2025-06-05',
      },
    ];
    
    // GET handler for fetching tasks
    export async function GET() {
      console.log('Fetching tasks from mock API...');
      await simulateDelay(1500); // Simulate network latency
    
      return NextResponse.json(mockTasks);
    }
    

    Key points about Route Handlers:

    • They live in the app/api directory.
    • The file name route.ts (or route.js) is special and defines the handler for the route segment.
    • Exported functions like GET, POST, PUT, DELETE handle specific HTTP methods.
    • NextResponse.json() is used to return JSON data.
    • We added a simulateDelay to mimic real-world network requests, which will be useful for testing our loading UI.

2. Update app/dashboard/page.tsx (Server Component for Data Fetching)

Now, we'll modify our dashboard page to fetch tasks from the mock API on the server and then pass them down to the TaskList component.

app/dashboard/page.tsx (Update this file)

// app/dashboard/page.tsx
// This is the dashboard page, now a Server Component that fetches tasks.
// It will fetch data and pass it to the TaskList Client Component.

import TaskList from '../../components/TaskList'; // Import TaskList
import { Metadata } from 'next'; // For page-specific metadata

// Define the type for our fetched tasks (should match MockTask from route.ts)
interface FetchedTask {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: 'Low' | 'Medium' | 'High';
  dueDate: string;
}

// Metadata for this specific page (optional, but good practice for SEO)
export const metadata: Metadata = {
  title: 'Dashboard | My TaskFlow',
  description: 'View and manage all your tasks from your personalized dashboard.',
};

// This is an async Server Component.
// It will fetch data before rendering the page.
export default async function DashboardPage() {
  let tasks: FetchedTask[] = [];
  let error: string | null = null;

  try {
    // Fetch data from our mock API endpoint
    // Next.js automatically caches fetch requests, which is great for performance.
    // For this demonstration, we'll assume default caching for now.
    const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/tasks`, {
      cache: 'no-store', // This ensures data is always fresh on every request (SSR behavior)
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch tasks: ${response.statusText}`);
    }

    tasks = await response.json();
  } catch (err) {
    error = err instanceof Error ? err.message : 'An unknown error occurred.';
    console.error('Error fetching tasks:', error);
  }

  return (
    <div className="flex flex-col items-center p-4">
      <h2 className="text-3xl font-bold text-gray-800 mb-6">Your Dashboard</h2>
      <p className="text-lg text-gray-600 mb-8">
        Manage your tasks efficiently right from here.
      </p>

      {error ? (
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
          <strong className="font-bold">Error!</strong>
          <span className="block sm:inline"> {error}</span>
        </div>
      ) : (
        // Pass the fetched tasks as props to the TaskList Client Component
        <TaskList initialTasks={tasks} />
      )}
    </div>
  );
}

Important Notes:

  • We've made DashboardPage an async function, allowing it to use await for fetch.
  • The fetch call is made directly inside the Server Component.
  • process.env.NEXT_PUBLIC_BASE_URL is used for the API URL. You might need to add NEXT_PUBLIC_BASE_URL=http://localhost:3000 to your .env.local file for development.
  • We've added cache: 'no-store' to the fetch options. This explicitly tells Next.js not to cache the response for this specific fetch call, ensuring that the tasks are always fetched fresh on every request. This behavior makes the component effectively Server-Side Rendered (SSR) for this data. Without no-store, Next.js might default to caching (SSG-like behavior), which is not what we want for frequently changing user tasks.
  • Error handling for the fetch request is included.

3. Modify components/TaskList.tsx (Pure UI Component)

Now that app/dashboard/page.tsx is responsible for fetching the initial tasks, our TaskList component no longer needs its own useState for tasks or the useEffect for initial loading. It will simply receive the initialTasks as a prop. The addTask, toggleTaskCompletion, and deleteTask functions will still operate on the internal tasks state for now, but in Module 6 (Server Actions), these will be refactored to interact with the server.

components/TaskList.tsx (Update this file)

// components/TaskList.tsx
// This Client Component now receives initial tasks as props from a Server Component.
// Its state management for tasks (add, toggle, delete) remains client-side for now,
// but will be refactored with Server Actions in a future module.
'use client';

import { useState, useEffect, FormEvent } from 'react';

// Re-use the Task interface definition for consistency
interface Task {
  id: string;
  title: string;
  description: string; // Added description based on mock data
  completed: boolean;
  priority: 'Low' | 'Medium' | 'High'; // Added priority
  dueDate: string; // Added dueDate
}

// Define props for TaskList component
interface TaskListProps {
  initialTasks: Task[];
}

export default function TaskList({ initialTasks }: TaskListProps) {
  // Initialize tasks state with the `initialTasks` passed as prop
  const [tasks, setTasks] = useState<Task[]>(initialTasks);
  const [newTaskTitle, setNewTaskTitle] = useState<string>('');

  // Update internal state if initialTasks prop changes (e.g., re-fetching from server)
  useEffect(() => {
    setTasks(initialTasks);
  }, [initialTasks]);

  // Function to add a new task (client-side only for now)
  const addTask = (event: FormEvent) => {
    event.preventDefault();

    if (newTaskTitle.trim() === '') {
      console.log('Task title cannot be empty.');
      // In a real app, you would show a user-friendly error message (e.g., a toast or modal)
      return;
    }
    const newTask: Task = {
      id: crypto.randomUUID(), // Use a more robust unique ID
      title: newTaskTitle,
      description: 'No description provided.', // Default description
      completed: false,
      priority: 'Medium', // Default priority
      dueDate: new Date().toISOString().split('T')[0], // Default to today's date
    };
    setTasks([...tasks, newTask]);
    setNewTaskTitle('');
  };

  // Function to toggle task completion status (client-side only for now)
  const toggleTaskCompletion = (id: string) => {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  // Function to delete a task (client-side only for now)
  const deleteTask = (id: string) => {
    setTasks(tasks.filter((task) => task.id !== id));
  };

  return (
    <div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-xl border border-gray-200">
      <h3 className="text-3xl font-bold text-gray-800 mb-6">Your Tasks</h3>

      {/* Task Input Form */}
      <form onSubmit={addTask} className="flex flex-col md:flex-row gap-4 mb-8">
        <input
          type="text"
          placeholder="Add a new task..."
          value={newTaskTitle}
          onChange={(e) => setNewTaskTitle(e.target.value)}
          className="flex-grow p-3 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 outline-none"
        />
        <button
          type="submit"
          className="px-6 py-3 bg-blue-600 text-white rounded-md shadow-md hover:bg-blue-700 transition duration-300 transform hover:scale-105"
        >
          Add Task
        </button>
      </form>

      {/* Task List */}
      {tasks.length === 0 ? (
        <p className="text-center text-gray-500 italic">No tasks yet. Start adding some!</p>
      ) : (
        <ul className="space-y-4">
          {tasks.map((task) => (
            <li
              key={task.id}
              className="flex items-center justify-between p-4 bg-gray-50 border border-gray-200 rounded-md shadow-sm transition duration-200 hover:bg-gray-100"
            >
              <div className="flex items-start flex-grow"> {/* Use flex-start to align checkbox and text */}
                <input
                  type="checkbox"
                  checked={task.completed}
                  onChange={() => toggleTaskCompletion(task.id)}
                  className="form-checkbox h-5 w-5 text-blue-600 rounded mr-3 mt-1 cursor-pointer flex-shrink-0"
                />
                <div className="flex flex-col flex-grow">
                  <span
                    className={`text-lg font-medium ${
                      task.completed ? 'line-through text-gray-500' : 'text-gray-900'
                    }`}
                  >
                    {task.title}
                  </span>
                  <p className="text-sm text-gray-500 mt-1">
                    Priority: <span className="font-semibold">{task.priority}</span> | Due: <span className="font-semibold">{task.dueDate}</span>
                  </p>
                  <p className="text-sm text-gray-600 mt-1 italic">{task.description}</p>
                </div>
              </div>
              <button
                onClick={() => deleteTask(task.id)}
                className="ml-4 px-3 py-1 bg-red-500 text-white rounded-md text-sm shadow-sm hover:bg-red-600 transition duration-300 flex-shrink-0"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

4. Create app/dashboard/loading.tsx

To provide a good user experience during the data fetching delay, we'll create a loading skeleton for our dashboard.

  1. Create a new file loading.tsx inside your app/dashboard/ folder:

    my-task-app/
    └── app/
        ├── ...
        └── dashboard/
            ├── layout.tsx
            ├── page.tsx
            ├── loading.tsx     <-- NEW FILE
    
  2. Add the following content to app/dashboard/loading.tsx:

    app/dashboard/loading.tsx

    // app/dashboard/loading.tsx
    // This file defines the loading UI for the /dashboard route segment.
    // It will be displayed automatically while data is being fetched for page.tsx.
    
    import React from 'react';
    
    export default function DashboardLoading() {
      return (
        <div className="flex flex-col items-center justify-center p-8 bg-white rounded-xl shadow-lg border border-gray-200 animate-pulse">
          <h2 className="text-3xl font-bold text-gray-400 mb-6">Loading Your Dashboard...</h2>
          <div className="h-6 bg-gray-200 rounded w-3/4 mb-4"></div>
          <div className="h-4 bg-gray-200 rounded w-1/2 mb-8"></div>
    
          <div className="w-full max-w-2xl mx-auto space-y-4">
            {/* Skeleton for task input form */}
            <div className="flex flex-col md:flex-row gap-4 mb-8">
              <div className="h-12 bg-gray-200 rounded-md flex-grow"></div>
              <div className="h-12 w-32 bg-gray-300 rounded-md"></div>
            </div>
    
            {/* Skeletons for individual tasks */}
            {[...Array(3)].map((_, i) => ( // Render 3 skeleton items
              <div key={i} className="flex items-center justify-between p-4 bg-gray-100 border border-gray-200 rounded-md shadow-sm">
                <div className="flex items-start flex-grow">
                  <div className="h-5 w-5 bg-gray-300 rounded-full mr-3 mt-1"></div>
                  <div className="flex flex-col flex-grow">
                    <div className="h-6 bg-gray-300 rounded w-3/4 mb-2"></div>
                    <div className="h-4 bg-gray-200 rounded w-1/2"></div>
                  </div>
                </div>
                <div className="ml-4 h-8 w-16 bg-gray-300 rounded-md"></div>
              </div>
            ))}
          </div>
        </div>
      );
    }
    

What we've done

  • Mock API: Created a basic GET Route Handler at /api/tasks that returns hardcoded task data with a simulated delay.
  • Server-Side Fetching: Modified app/dashboard/page.tsx to be an async Server Component that fetches data from our mock API. We used cache: 'no-store' to enforce SSR behavior.
  • Prop-Based TaskList: Updated components/TaskList.tsx to receive initialTasks as a prop, making it a pure UI component (for initial render) while still allowing client-side interactions for now.
  • Loading UI: Implemented app/dashboard/loading.tsx to display a skeleton loader while the tasks are being fetched, significantly improving user experience.

To test the new data fetching and loading states:

  1. Ensure your development server is running (npm run dev).
  2. If you haven't already, create a .env.local file in your project root and add:

    NEXT_PUBLIC_BASE_URL=http://localhost:3000
    

    Then restart your dev server (npm run dev) if it was already running.

  3. Navigate to http://localhost:3000/dashboard.

You should now briefly see a loading skeleton on the dashboard page before the mock tasks appear. This demonstrates a fundamental pattern for data fetching in Next.js using Server Components and handling loading states gracefully. In the next module, we'll learn about data mutations using Server Actions, which provide a streamlined way to update data on the server without explicit API routes.